github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/utils"
    19  	corecharm "gopkg.in/juju/charm.v6-unstable"
    20  
    21  	"github.com/juju/juju/apiserver/params"
    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  	UnitTag  string         `json:"unit-tag"`
    79  }
    80  
    81  // APIMetricBatch converts the specified MetricBatch to a params.MetricBatch,
    82  // which can then be sent to the state server.
    83  func APIMetricBatch(batch MetricBatch) params.MetricBatchParam {
    84  	metrics := make([]params.Metric, len(batch.Metrics))
    85  	for i, metric := range batch.Metrics {
    86  		metrics[i] = params.Metric{Key: metric.Key, Value: metric.Value, Time: metric.Time}
    87  	}
    88  	return params.MetricBatchParam{
    89  		Tag: batch.UnitTag,
    90  		Batch: params.MetricBatch{
    91  			UUID:     batch.UUID,
    92  			CharmURL: batch.CharmURL,
    93  			Created:  batch.Created,
    94  			Metrics:  metrics,
    95  		},
    96  	}
    97  }
    98  
    99  // MetricMetadata is used to store metadata for the current metric batch.
   100  type MetricMetadata struct {
   101  	CharmURL string    `json:"charmurl"`
   102  	UUID     string    `json:"uuid"`
   103  	Created  time.Time `json:"created"`
   104  	UnitTag  string    `json:"unit-tag"`
   105  }
   106  
   107  // JSONMetricRecorder implements the MetricsRecorder interface
   108  // and writes metrics to a spool directory for store-and-forward.
   109  type JSONMetricRecorder struct {
   110  	spoolDir     string
   111  	validMetrics map[string]corecharm.Metric
   112  	charmURL     string
   113  	uuid         utils.UUID
   114  	created      time.Time
   115  	unitTag      string
   116  
   117  	lock sync.Mutex
   118  
   119  	file io.Closer
   120  	enc  *json.Encoder
   121  }
   122  
   123  // MetricRecorderConfig stores configuration data for a metrics recorder.
   124  type MetricRecorderConfig struct {
   125  	SpoolDir string
   126  	Metrics  map[string]corecharm.Metric
   127  	CharmURL string
   128  	UnitTag  string
   129  }
   130  
   131  // NewJSONMetricRecorder creates a new JSON metrics recorder.
   132  func NewJSONMetricRecorder(config MetricRecorderConfig) (rec *JSONMetricRecorder, rErr error) {
   133  	mbUUID, err := utils.NewUUID()
   134  	if err != nil {
   135  		return nil, errors.Trace(err)
   136  	}
   137  
   138  	recorder := &JSONMetricRecorder{
   139  		spoolDir:     config.SpoolDir,
   140  		uuid:         mbUUID,
   141  		charmURL:     config.CharmURL,
   142  		created:      time.Now().UTC(),
   143  		validMetrics: config.Metrics,
   144  		unitTag:      config.UnitTag,
   145  	}
   146  	if err := recorder.open(); err != nil {
   147  		return nil, errors.Trace(err)
   148  	}
   149  	return recorder, nil
   150  }
   151  
   152  // Close implements the MetricsRecorder interface.
   153  func (m *JSONMetricRecorder) Close() error {
   154  	m.lock.Lock()
   155  	defer m.lock.Unlock()
   156  
   157  	err := m.file.Close()
   158  	if err != nil {
   159  		return errors.Trace(err)
   160  	}
   161  
   162  	// We have an exclusive lock on this metric batch here, because
   163  	// metricsFile.Close was able to rename the final filename atomically.
   164  	//
   165  	// Now write the meta file so that JSONMetricReader discovers a finished
   166  	// pair of files.
   167  	err = m.recordMetaData()
   168  	if err != nil {
   169  		return errors.Trace(err)
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  // AddMetric implements the MetricsRecorder interface.
   176  func (m *JSONMetricRecorder) AddMetric(key, value string, created time.Time) error {
   177  	if !m.IsDeclaredMetric(key) {
   178  		return errors.Errorf("metric key %q not declared by the charm", key)
   179  	}
   180  	m.lock.Lock()
   181  	defer m.lock.Unlock()
   182  	return errors.Trace(m.enc.Encode(jujuc.Metric{Key: key, Value: value, Time: created}))
   183  }
   184  
   185  // IsDeclaredMetric returns true if the metric recorder is permitted to store this metric.
   186  // Returns false if the uniter using this recorder doesn't define this metric.
   187  func (m *JSONMetricRecorder) IsDeclaredMetric(key string) bool {
   188  	_, ok := m.validMetrics[key]
   189  	return ok
   190  }
   191  
   192  func (m *JSONMetricRecorder) open() error {
   193  	dataFile := filepath.Join(m.spoolDir, m.uuid.String())
   194  	if _, err := os.Stat(dataFile); err != nil && !os.IsNotExist(err) {
   195  		if err != nil {
   196  			return errors.Annotatef(err, "failed to stat file %s", dataFile)
   197  		}
   198  		return errors.Errorf("file %s already exists", dataFile)
   199  	}
   200  
   201  	dataWriter, err := createMetricFile(dataFile)
   202  	if err != nil {
   203  		return errors.Trace(err)
   204  	}
   205  	m.file = dataWriter
   206  	m.enc = json.NewEncoder(dataWriter)
   207  	return nil
   208  }
   209  
   210  func checkSpoolDir(path string) error {
   211  	if _, err := os.Stat(path); os.IsNotExist(err) {
   212  		err := os.MkdirAll(path, 0755)
   213  		if err != nil {
   214  			return errors.Trace(err)
   215  		}
   216  	} else if err != nil {
   217  		return errors.Trace(err)
   218  	}
   219  	return nil
   220  }
   221  
   222  func (m *JSONMetricRecorder) recordMetaData() error {
   223  	metaFile := filepath.Join(m.spoolDir, fmt.Sprintf("%s.meta", m.uuid.String()))
   224  	if _, err := os.Stat(metaFile); !os.IsNotExist(err) {
   225  		if err != nil {
   226  			return errors.Annotatef(err, "failed to stat file %s", metaFile)
   227  		}
   228  		return errors.Errorf("file %s already exists", metaFile)
   229  	}
   230  
   231  	metadata := MetricMetadata{
   232  		CharmURL: m.charmURL,
   233  		UUID:     m.uuid.String(),
   234  		Created:  m.created,
   235  		UnitTag:  m.unitTag,
   236  	}
   237  	// The use of a metricFile here ensures that the JSONMetricReader will only
   238  	// find a fully-written metafile.
   239  	metaWriter, err := createMetricFile(metaFile)
   240  	if err != nil {
   241  		return errors.Trace(err)
   242  	}
   243  	defer metaWriter.Close()
   244  	enc := json.NewEncoder(metaWriter)
   245  	err = enc.Encode(metadata)
   246  	if err != nil {
   247  		return errors.Trace(err)
   248  	}
   249  	return nil
   250  }
   251  
   252  // JSONMetricsReader reads metrics batches stored in the spool directory.
   253  type JSONMetricReader struct {
   254  	dir string
   255  }
   256  
   257  // NewJSONMetricsReader creates a new JSON metrics reader for the specified spool directory.
   258  func NewJSONMetricReader(spoolDir string) (*JSONMetricReader, error) {
   259  	if _, err := os.Stat(spoolDir); err != nil {
   260  		return nil, errors.Annotatef(err, "failed to open spool directory %q", spoolDir)
   261  	}
   262  	return &JSONMetricReader{
   263  		dir: spoolDir,
   264  	}, nil
   265  }
   266  
   267  // Read implements the MetricsReader interface.
   268  // Due to the way the batches are stored in the file system,
   269  // they will be returned in an arbitrary order. This does not affect the behavior.
   270  func (r *JSONMetricReader) Read() ([]MetricBatch, error) {
   271  	var batches []MetricBatch
   272  
   273  	walker := func(path string, info os.FileInfo, err error) error {
   274  		if err != nil {
   275  			return errors.Trace(err)
   276  		}
   277  		if info.IsDir() && path != r.dir {
   278  			return filepath.SkipDir
   279  		} else if !strings.HasSuffix(info.Name(), ".meta") {
   280  			return nil
   281  		}
   282  
   283  		batch, err := decodeBatch(path)
   284  		if err != nil {
   285  			return errors.Trace(err)
   286  		}
   287  		batch.Metrics, err = decodeMetrics(filepath.Join(r.dir, batch.UUID))
   288  		if err != nil {
   289  			return errors.Trace(err)
   290  		}
   291  		if len(batch.Metrics) > 0 {
   292  			batches = append(batches, batch)
   293  		}
   294  		return nil
   295  	}
   296  	if err := filepath.Walk(r.dir, walker); err != nil {
   297  		return nil, errors.Trace(err)
   298  	}
   299  	return batches, nil
   300  }
   301  
   302  // Remove implements the MetricsReader interface.
   303  func (r *JSONMetricReader) Remove(uuid string) error {
   304  	metaFile := filepath.Join(r.dir, fmt.Sprintf("%s.meta", uuid))
   305  	dataFile := filepath.Join(r.dir, uuid)
   306  	err := os.Remove(metaFile)
   307  	if err != nil && !os.IsNotExist(err) {
   308  		return errors.Trace(err)
   309  	}
   310  	err = os.Remove(dataFile)
   311  	if err != nil {
   312  		return errors.Trace(err)
   313  	}
   314  	return nil
   315  }
   316  
   317  // Close implements the MetricsReader interface.
   318  func (r *JSONMetricReader) Close() error {
   319  	return nil
   320  }
   321  
   322  func decodeBatch(file string) (MetricBatch, error) {
   323  	var batch MetricBatch
   324  	f, err := os.Open(file)
   325  	if err != nil {
   326  		return MetricBatch{}, errors.Trace(err)
   327  	}
   328  	defer f.Close()
   329  	dec := json.NewDecoder(f)
   330  	err = dec.Decode(&batch)
   331  	if err != nil {
   332  		return MetricBatch{}, errors.Trace(err)
   333  	}
   334  	return batch, nil
   335  }
   336  
   337  func decodeMetrics(file string) ([]jujuc.Metric, error) {
   338  	var metrics []jujuc.Metric
   339  	f, err := os.Open(file)
   340  	if err != nil {
   341  		return nil, errors.Trace(err)
   342  	}
   343  	defer f.Close()
   344  	dec := json.NewDecoder(f)
   345  	for {
   346  		var metric jujuc.Metric
   347  		err := dec.Decode(&metric)
   348  		if err == io.EOF {
   349  			break
   350  		} else if err != nil {
   351  			return nil, errors.Trace(err)
   352  		}
   353  		metrics = append(metrics, metric)
   354  	}
   355  	return metrics, nil
   356  }