github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/worker/uniter/metrics/metrics.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package metrics
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/juju/apiserver/params"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/utils"
    20  	corecharm "gopkg.in/juju/charm.v5"
    21  
    22  	"github.com/juju/juju/worker/uniter/runner/jujuc"
    23  )
    24  
    25  var logger = loggo.GetLogger("juju.worker.uniter.metrics")
    26  
    27  type metricFile struct {
    28  	*os.File
    29  	finalName string
    30  }
    31  
    32  func createMetricFile(path string) (*metricFile, error) {
    33  	dir, base := filepath.Dir(path), filepath.Base(path)
    34  	if !filepath.IsAbs(dir) {
    35  		return nil, errors.Errorf("not an absolute path: %q", path)
    36  	}
    37  
    38  	workUUID, err := utils.NewUUID()
    39  	if err != nil {
    40  		return nil, errors.Trace(err)
    41  	}
    42  	workName := filepath.Join(dir, fmt.Sprintf(".%s.inc-%s", base, workUUID.String()))
    43  
    44  	f, err := os.Create(workName)
    45  	if err != nil {
    46  		return nil, errors.Trace(err)
    47  	}
    48  	return &metricFile{File: f, finalName: path}, nil
    49  }
    50  
    51  // Close implements io.Closer.
    52  func (f *metricFile) Close() error {
    53  	err := f.File.Close()
    54  	if err != nil {
    55  		return errors.Trace(err)
    56  	}
    57  	ok, err := utils.MoveFile(f.Name(), f.finalName)
    58  	if err != nil {
    59  		// ok can be true even when there is an error completing the move, on
    60  		// platforms that implement it in multiple steps that can fail
    61  		// separately. POSIX for example, uses link(2) to claim the new
    62  		// location atomically, followed by an unlink(2) to release the old
    63  		// location.
    64  		if !ok {
    65  			return errors.Trace(err)
    66  		}
    67  		logger.Errorf("failed to remove temporary file %q: %v", f.Name(), err)
    68  	}
    69  	return nil
    70  }
    71  
    72  // MetricBatch stores the information relevant to a single metrics batch.
    73  type MetricBatch struct {
    74  	CharmURL string         `json:"charmurl"`
    75  	UUID     string         `json:"uuid"`
    76  	Created  time.Time      `json:"created"`
    77  	Metrics  []jujuc.Metric `json:"metrics"`
    78  }
    79  
    80  // APIMetricBatch converts the specified MetricBatch to a params.MetricBatch,
    81  // which can then be sent to the state server.
    82  func APIMetricBatch(batch MetricBatch) params.MetricBatch {
    83  	metrics := make([]params.Metric, len(batch.Metrics))
    84  	for i, metric := range batch.Metrics {
    85  		metrics[i] = params.Metric{Key: metric.Key, Value: metric.Value, Time: metric.Time}
    86  	}
    87  	return params.MetricBatch{
    88  		UUID:     batch.UUID,
    89  		CharmURL: batch.CharmURL,
    90  		Created:  batch.Created,
    91  		Metrics:  metrics,
    92  	}
    93  }
    94  
    95  // MetricsMetadata is used to store metadata for the current metric batch.
    96  type MetricsMetadata struct {
    97  	CharmURL string    `json:"charmurl"`
    98  	UUID     string    `json:"uuid"`
    99  	Created  time.Time `json:"created"`
   100  }
   101  
   102  // JSONMetricRecorder implements the MetricsRecorder interface
   103  // and writes metrics to a spool directory for store-and-forward.
   104  type JSONMetricRecorder struct {
   105  	spoolDir     string
   106  	validMetrics map[string]corecharm.Metric
   107  	charmURL     string
   108  	uuid         utils.UUID
   109  	created      time.Time
   110  
   111  	lock sync.Mutex
   112  
   113  	file io.Closer
   114  	enc  *json.Encoder
   115  }
   116  
   117  // NewJSONMetricRecorder creates a new JSON metrics recorder.
   118  // It checks if the metrics spool directory exists, if it does not - it is created. Then
   119  // it tries to find an unused metric batch UUID 3 times.
   120  func NewJSONMetricRecorder(spoolDir string, metrics map[string]corecharm.Metric, charmURL string) (rec *JSONMetricRecorder, rErr error) {
   121  	if err := checkSpoolDir(spoolDir); err != nil {
   122  		return nil, errors.Trace(err)
   123  	}
   124  
   125  	mbUUID, err := utils.NewUUID()
   126  	if err != nil {
   127  		return nil, errors.Trace(err)
   128  	}
   129  
   130  	recorder := &JSONMetricRecorder{
   131  		spoolDir:     spoolDir,
   132  		uuid:         mbUUID,
   133  		charmURL:     charmURL,
   134  		created:      time.Now().UTC(),
   135  		validMetrics: metrics,
   136  	}
   137  	if err := recorder.open(); err != nil {
   138  		return nil, errors.Trace(err)
   139  	}
   140  	return recorder, nil
   141  }
   142  
   143  // Close implements the MetricsRecorder interface.
   144  func (m *JSONMetricRecorder) Close() error {
   145  	m.lock.Lock()
   146  	defer m.lock.Unlock()
   147  
   148  	err := m.file.Close()
   149  	if err != nil {
   150  		return errors.Trace(err)
   151  	}
   152  
   153  	// We have an exclusive lock on this metric batch here, because
   154  	// metricsFile.Close was able to rename the final filename atomically.
   155  	//
   156  	// Now write the meta file so that JSONMetricReader discovers a finished
   157  	// pair of files.
   158  	err = m.recordMetaData()
   159  	if err != nil {
   160  		return errors.Trace(err)
   161  	}
   162  
   163  	return nil
   164  }
   165  
   166  // AddMetric implements the MetricsRecorder interface.
   167  func (m *JSONMetricRecorder) AddMetric(key, value string, created time.Time) error {
   168  	if !m.IsDeclaredMetric(key) {
   169  		return errors.Errorf("metric key %q not declared by the charm", key)
   170  	}
   171  	m.lock.Lock()
   172  	defer m.lock.Unlock()
   173  	return errors.Trace(m.enc.Encode(jujuc.Metric{Key: key, Value: value, Time: created}))
   174  }
   175  
   176  // IsDeclaredMetric returns true if the metric recorder is permitted to store this metric.
   177  // Returns false if the uniter using this recorder doesn't define this metric.
   178  func (m *JSONMetricRecorder) IsDeclaredMetric(key string) bool {
   179  	_, ok := m.validMetrics[key]
   180  	return ok
   181  }
   182  
   183  func (m *JSONMetricRecorder) open() error {
   184  	dataFile := filepath.Join(m.spoolDir, m.uuid.String())
   185  	if _, err := os.Stat(dataFile); err != nil && !os.IsNotExist(err) {
   186  		if err != nil {
   187  			return errors.Annotatef(err, "failed to stat file %s", dataFile)
   188  		}
   189  		return errors.Errorf("file %s already exists", dataFile)
   190  	}
   191  
   192  	dataWriter, err := createMetricFile(dataFile)
   193  	if err != nil {
   194  		return errors.Trace(err)
   195  	}
   196  	m.file = dataWriter
   197  	m.enc = json.NewEncoder(dataWriter)
   198  	return nil
   199  }
   200  
   201  func checkSpoolDir(path string) error {
   202  	if _, err := os.Stat(path); os.IsNotExist(err) {
   203  		err := os.MkdirAll(path, 0755)
   204  		if err != nil {
   205  			return errors.Trace(err)
   206  		}
   207  	} else if err != nil {
   208  		return errors.Trace(err)
   209  	}
   210  	return nil
   211  }
   212  
   213  func (m *JSONMetricRecorder) recordMetaData() error {
   214  	metaFile := filepath.Join(m.spoolDir, fmt.Sprintf("%s.meta", m.uuid.String()))
   215  	if _, err := os.Stat(metaFile); !os.IsNotExist(err) {
   216  		if err != nil {
   217  			return errors.Annotatef(err, "failed to stat file %s", metaFile)
   218  		}
   219  		return errors.Errorf("file %s already exists", metaFile)
   220  	}
   221  
   222  	metadata := MetricsMetadata{
   223  		CharmURL: m.charmURL,
   224  		UUID:     m.uuid.String(),
   225  		Created:  m.created,
   226  	}
   227  	// The use of a metricFile here ensures that the JSONMetricReader will only
   228  	// find a fully-written metafile.
   229  	metaWriter, err := createMetricFile(metaFile)
   230  	if err != nil {
   231  		return errors.Trace(err)
   232  	}
   233  	defer metaWriter.Close()
   234  	enc := json.NewEncoder(metaWriter)
   235  	err = enc.Encode(metadata)
   236  	if err != nil {
   237  		return errors.Trace(err)
   238  	}
   239  	return nil
   240  }
   241  
   242  // JSONMetricsReader reads metrics batches stored in the spool directory.
   243  type JSONMetricReader struct {
   244  	dir string
   245  }
   246  
   247  // NewJSONMetricsReader creates a new JSON metrics reader for the specified spool directory.
   248  func NewJSONMetricReader(spoolDir string) (*JSONMetricReader, error) {
   249  	if _, err := os.Stat(spoolDir); err != nil {
   250  		return nil, errors.Annotatef(err, "failed to open spool directory %q", spoolDir)
   251  	}
   252  	return &JSONMetricReader{
   253  		dir: spoolDir,
   254  	}, nil
   255  }
   256  
   257  // Read implements the MetricsReader interface.
   258  // Due to the way the batches are stored in the file system,
   259  // they will be returned in an arbitrary order. This does not affect the behavior.
   260  func (r *JSONMetricReader) Read() ([]MetricBatch, error) {
   261  	var batches []MetricBatch
   262  
   263  	walker := func(path string, info os.FileInfo, err error) error {
   264  		if err != nil {
   265  			return errors.Trace(err)
   266  		}
   267  		if info.IsDir() && path != r.dir {
   268  			return filepath.SkipDir
   269  		} else if !strings.HasSuffix(info.Name(), ".meta") {
   270  			return nil
   271  		}
   272  
   273  		batch, err := decodeBatch(path)
   274  		if err != nil {
   275  			return errors.Trace(err)
   276  		}
   277  		batch.Metrics, err = decodeMetrics(filepath.Join(r.dir, batch.UUID))
   278  		if err != nil {
   279  			return errors.Trace(err)
   280  		}
   281  		if len(batch.Metrics) > 0 {
   282  			batches = append(batches, batch)
   283  		}
   284  		return nil
   285  	}
   286  	if err := filepath.Walk(r.dir, walker); err != nil {
   287  		return nil, errors.Trace(err)
   288  	}
   289  	return batches, nil
   290  }
   291  
   292  // Remove implements the MetricsReader interface.
   293  func (r *JSONMetricReader) Remove(uuid string) error {
   294  	metaFile := filepath.Join(r.dir, fmt.Sprintf("%s.meta", uuid))
   295  	dataFile := filepath.Join(r.dir, uuid)
   296  	err := os.Remove(metaFile)
   297  	if err != nil && !os.IsNotExist(err) {
   298  		return errors.Trace(err)
   299  	}
   300  	err = os.Remove(dataFile)
   301  	if err != nil {
   302  		return errors.Trace(err)
   303  	}
   304  	return nil
   305  }
   306  
   307  // Close implements the MetricsReader interface.
   308  func (r *JSONMetricReader) Close() error {
   309  	return nil
   310  }
   311  
   312  func decodeBatch(file string) (MetricBatch, error) {
   313  	var batch MetricBatch
   314  	f, err := os.Open(file)
   315  	if err != nil {
   316  		return MetricBatch{}, errors.Trace(err)
   317  	}
   318  	defer f.Close()
   319  	dec := json.NewDecoder(f)
   320  	err = dec.Decode(&batch)
   321  	if err != nil {
   322  		return MetricBatch{}, errors.Trace(err)
   323  	}
   324  	return batch, nil
   325  }
   326  
   327  func decodeMetrics(file string) ([]jujuc.Metric, error) {
   328  	var metrics []jujuc.Metric
   329  	f, err := os.Open(file)
   330  	if err != nil {
   331  		return nil, errors.Trace(err)
   332  	}
   333  	defer f.Close()
   334  	dec := json.NewDecoder(f)
   335  	for {
   336  		var metric jujuc.Metric
   337  		err := dec.Decode(&metric)
   338  		if err == io.EOF {
   339  			break
   340  		} else if err != nil {
   341  			return nil, errors.Trace(err)
   342  		}
   343  		metrics = append(metrics, metric)
   344  	}
   345  	return metrics, nil
   346  }