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 }