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 }