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