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