github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/worker/metrics/spool/metrics.go (about)

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