github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/worker/metrics/collect/manifold.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Package collect provides a worker that executes the collect-metrics hook
     5  // periodically, as long as the workload has been started (between start and
     6  // stop hooks). collect-metrics executes in its own execution context, which is
     7  // restricted to avoid contention with uniter "lifecycle" hooks.
     8  package collect
     9  
    10  import (
    11  	"fmt"
    12  	"path"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/juju/errors"
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/os"
    19  	corecharm "gopkg.in/juju/charm.v6"
    20  	"gopkg.in/juju/charm.v6/hooks"
    21  	"gopkg.in/juju/names.v2"
    22  	"gopkg.in/juju/worker.v1"
    23  	"gopkg.in/juju/worker.v1/dependency"
    24  
    25  	"github.com/juju/juju/agent"
    26  	jworker "github.com/juju/juju/worker"
    27  	"github.com/juju/juju/worker/fortress"
    28  	"github.com/juju/juju/worker/metrics/spool"
    29  	"github.com/juju/juju/worker/uniter"
    30  	"github.com/juju/juju/worker/uniter/charm"
    31  	"github.com/juju/juju/worker/uniter/runner"
    32  	"github.com/juju/juju/worker/uniter/runner/context"
    33  )
    34  
    35  const (
    36  	defaultSocketName = "metrics-collect.socket"
    37  )
    38  
    39  var (
    40  	logger        = loggo.GetLogger("juju.worker.metrics.collect")
    41  	defaultPeriod = 5 * time.Minute
    42  
    43  	// errMetricsNotDefined is returned when the charm the uniter is running does
    44  	// not declared any metrics.
    45  	errMetricsNotDefined = errors.New("no metrics defined")
    46  
    47  	// readCharm function reads the charm directory and extracts declared metrics and the charm url.
    48  	readCharm = func(unitTag names.UnitTag, paths context.Paths) (*corecharm.URL, map[string]corecharm.Metric, error) {
    49  		ch, err := corecharm.ReadCharm(paths.GetCharmDir())
    50  		if err != nil {
    51  			return nil, nil, errors.Annotatef(err, "failed to read charm from: %v", paths.GetCharmDir())
    52  		}
    53  		chURL, err := charm.ReadCharmURL(path.Join(paths.GetCharmDir(), charm.CharmURLPath))
    54  		if err != nil {
    55  			return nil, nil, errors.Trace(err)
    56  		}
    57  		charmMetrics := map[string]corecharm.Metric{}
    58  		if ch.Metrics() != nil {
    59  			charmMetrics = ch.Metrics().Metrics
    60  		}
    61  		return chURL, charmMetrics, nil
    62  	}
    63  
    64  	// newRecorder returns a struct that implements the spool.MetricRecorder
    65  	// interface.
    66  	newRecorder = func(unitTag names.UnitTag, paths context.Paths, metricFactory spool.MetricFactory) (spool.MetricRecorder, error) {
    67  		chURL, charmMetrics, err := readCharm(unitTag, paths)
    68  		if err != nil {
    69  			return nil, errors.Trace(err)
    70  		}
    71  		if len(charmMetrics) == 0 {
    72  			return nil, errMetricsNotDefined
    73  		}
    74  		return metricFactory.Recorder(charmMetrics, chURL.String(), unitTag.String())
    75  	}
    76  
    77  	newSocketListener = func(path string, handler spool.ConnectionHandler) (stopper, error) {
    78  		return spool.NewSocketListener(path, handler)
    79  	}
    80  )
    81  
    82  type stopper interface {
    83  	Stop() error
    84  }
    85  
    86  // ManifoldConfig identifies the resource names upon which the collect manifold
    87  // depends.
    88  type ManifoldConfig struct {
    89  	Period *time.Duration
    90  
    91  	AgentName       string
    92  	MetricSpoolName string
    93  	CharmDirName    string
    94  }
    95  
    96  // Manifold returns a collect-metrics manifold.
    97  func Manifold(config ManifoldConfig) dependency.Manifold {
    98  	return dependency.Manifold{
    99  		Inputs: []string{
   100  			config.AgentName,
   101  			config.MetricSpoolName,
   102  			config.CharmDirName,
   103  		},
   104  		Start: func(context dependency.Context) (worker.Worker, error) {
   105  			collector, err := newCollect(config, context)
   106  			if err != nil {
   107  				return nil, err
   108  			}
   109  			return spool.NewPeriodicWorker(collector.Do, collector.period, jworker.NewTimer, collector.stop), nil
   110  		},
   111  	}
   112  }
   113  
   114  func socketName(baseDir, unitTag string) string {
   115  	if os.HostOS() == os.Windows {
   116  		return fmt.Sprintf(`\\.\pipe\collect-metrics-%s`, unitTag)
   117  	}
   118  	return path.Join(baseDir, defaultSocketName)
   119  }
   120  
   121  func newCollect(config ManifoldConfig, context dependency.Context) (*collect, error) {
   122  	period := defaultPeriod
   123  	if config.Period != nil {
   124  		period = *config.Period
   125  	}
   126  
   127  	var agent agent.Agent
   128  	if err := context.Get(config.AgentName, &agent); err != nil {
   129  		return nil, errors.Trace(err)
   130  	}
   131  
   132  	var metricFactory spool.MetricFactory
   133  	err := context.Get(config.MetricSpoolName, &metricFactory)
   134  	if err != nil {
   135  		return nil, errors.Trace(err)
   136  	}
   137  
   138  	var charmdir fortress.Guest
   139  	err = context.Get(config.CharmDirName, &charmdir)
   140  	if err != nil {
   141  		return nil, errors.Trace(err)
   142  	}
   143  	err = charmdir.Visit(func() error {
   144  		return nil
   145  	}, context.Abort())
   146  	if err != nil {
   147  		return nil, errors.Trace(err)
   148  	}
   149  
   150  	agentConfig := agent.CurrentConfig()
   151  	tag := agentConfig.Tag()
   152  	unitTag, ok := tag.(names.UnitTag)
   153  	if !ok {
   154  		return nil, errors.Errorf("expected a unit tag, got %v", tag)
   155  	}
   156  	paths := uniter.NewWorkerPaths(agentConfig.DataDir(), unitTag, "metrics-collect")
   157  	runner := &hookRunner{
   158  		unitTag: unitTag.String(),
   159  		paths:   paths,
   160  	}
   161  	var listener stopper
   162  	charmURL, validMetrics, err := readCharm(unitTag, paths)
   163  	if err != nil {
   164  		return nil, errors.Trace(err)
   165  	}
   166  
   167  	if len(validMetrics) > 0 && charmURL.Schema == "local" {
   168  		h := newHandler(handlerConfig{
   169  			charmdir:       charmdir,
   170  			agent:          agent,
   171  			unitTag:        unitTag,
   172  			metricsFactory: metricFactory,
   173  			runner:         runner,
   174  		})
   175  		listener, err = newSocketListener(socketName(paths.State.BaseDir, unitTag.String()), h)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  	}
   180  	collector := &collect{
   181  		period:        period,
   182  		agent:         agent,
   183  		metricFactory: metricFactory,
   184  		charmdir:      charmdir,
   185  		listener:      listener,
   186  		runner:        runner,
   187  	}
   188  
   189  	return collector, nil
   190  }
   191  
   192  type collect struct {
   193  	period        time.Duration
   194  	agent         agent.Agent
   195  	metricFactory spool.MetricFactory
   196  	charmdir      fortress.Guest
   197  	listener      stopper
   198  	runner        *hookRunner
   199  }
   200  
   201  func (w *collect) stop() {
   202  	if w.listener != nil {
   203  		w.listener.Stop()
   204  	}
   205  }
   206  
   207  // Do satisfies the worker.PeriodWorkerCall function type.
   208  func (w *collect) Do(stop <-chan struct{}) (err error) {
   209  	defer func() {
   210  		// See bug https://pad/lv/1733469
   211  		// If this function which is run by a PeriodicWorker
   212  		// exits with an error, we need to call stop() to
   213  		// ensure the listener socket is closed.
   214  		if err != nil {
   215  			w.stop()
   216  		}
   217  	}()
   218  
   219  	config := w.agent.CurrentConfig()
   220  	tag := config.Tag()
   221  	unitTag, ok := tag.(names.UnitTag)
   222  	if !ok {
   223  		return errors.Errorf("expected a unit tag, got %v", tag)
   224  	}
   225  	paths := uniter.NewWorkerPaths(config.DataDir(), unitTag, "metrics-collect")
   226  
   227  	recorder, err := newRecorder(unitTag, paths, w.metricFactory)
   228  	if errors.Cause(err) == errMetricsNotDefined {
   229  		logger.Tracef("%v", err)
   230  		return nil
   231  	} else if err != nil {
   232  		return errors.Annotate(err, "failed to instantiate metric recorder")
   233  	}
   234  
   235  	err = w.charmdir.Visit(func() error {
   236  		return w.runner.do(recorder)
   237  	}, stop)
   238  	if err == fortress.ErrAborted {
   239  		logger.Tracef("cannot execute collect-metrics: %v", err)
   240  		return nil
   241  	}
   242  	if spool.IsMetricsDataError(err) {
   243  		logger.Debugf("cannot record metrics: %v", err)
   244  		return nil
   245  	}
   246  	return err
   247  }
   248  
   249  type hookRunner struct {
   250  	m sync.Mutex
   251  
   252  	unitTag string
   253  	paths   uniter.Paths
   254  }
   255  
   256  func (h *hookRunner) do(recorder spool.MetricRecorder) error {
   257  	h.m.Lock()
   258  	defer h.m.Unlock()
   259  	logger.Tracef("recording metrics")
   260  
   261  	ctx := newHookContext(h.unitTag, recorder)
   262  	err := ctx.addJujuUnitsMetric()
   263  	if err != nil {
   264  		return errors.Annotatef(err, "error adding 'juju-units' metric")
   265  	}
   266  
   267  	r := runner.NewRunner(ctx, h.paths)
   268  	err = r.RunHook(string(hooks.CollectMetrics))
   269  	if err != nil {
   270  		return errors.Annotatef(err, "error running 'collect-metrics' hook")
   271  	}
   272  	return nil
   273  }