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