github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 "strconv" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/juju/errors" 18 "github.com/juju/loggo" 19 "github.com/juju/utils" 20 corecharm "gopkg.in/juju/charm.v6-unstable" 21 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/worker/uniter/runner/jujuc" 24 ) 25 26 var logger = loggo.GetLogger("juju.worker.uniter.metrics") 27 28 type metricFile struct { 29 *os.File 30 finalName string 31 } 32 33 func createMetricFile(path string) (*metricFile, error) { 34 dir, base := filepath.Dir(path), filepath.Base(path) 35 if !filepath.IsAbs(dir) { 36 return nil, errors.Errorf("not an absolute path: %q", path) 37 } 38 39 workUUID, err := utils.NewUUID() 40 if err != nil { 41 return nil, errors.Trace(err) 42 } 43 workName := filepath.Join(dir, fmt.Sprintf(".%s.inc-%s", base, workUUID.String())) 44 45 f, err := os.Create(workName) 46 if err != nil { 47 return nil, errors.Trace(err) 48 } 49 return &metricFile{File: f, finalName: path}, nil 50 } 51 52 // Close implements io.Closer. 53 func (f *metricFile) Close() error { 54 err := f.File.Close() 55 if err != nil { 56 return errors.Trace(err) 57 } 58 ok, err := utils.MoveFile(f.Name(), f.finalName) 59 if err != nil { 60 // ok can be true even when there is an error completing the move, on 61 // platforms that implement it in multiple steps that can fail 62 // separately. POSIX for example, uses link(2) to claim the new 63 // location atomically, followed by an unlink(2) to release the old 64 // location. 65 if !ok { 66 return errors.Trace(err) 67 } 68 logger.Errorf("failed to remove temporary file %q: %v", f.Name(), err) 69 } 70 return nil 71 } 72 73 // MetricBatch stores the information relevant to a single metrics batch. 74 type MetricBatch struct { 75 CharmURL string `json:"charmurl"` 76 UUID string `json:"uuid"` 77 Created time.Time `json:"created"` 78 Metrics []jujuc.Metric `json:"metrics"` 79 UnitTag string `json:"unit-tag"` 80 } 81 82 // APIMetricBatch converts the specified MetricBatch to a params.MetricBatch, 83 // which can then be sent to the controller. 84 func APIMetricBatch(batch MetricBatch) params.MetricBatchParam { 85 metrics := make([]params.Metric, len(batch.Metrics)) 86 for i, metric := range batch.Metrics { 87 metrics[i] = params.Metric{Key: metric.Key, Value: metric.Value, Time: metric.Time} 88 } 89 return params.MetricBatchParam{ 90 Tag: batch.UnitTag, 91 Batch: params.MetricBatch{ 92 UUID: batch.UUID, 93 CharmURL: batch.CharmURL, 94 Created: batch.Created, 95 Metrics: metrics, 96 }, 97 } 98 } 99 100 // MetricMetadata is used to store metadata for the current metric batch. 101 type MetricMetadata struct { 102 CharmURL string `json:"charmurl"` 103 UUID string `json:"uuid"` 104 Created time.Time `json:"created"` 105 UnitTag string `json:"unit-tag"` 106 } 107 108 // JSONMetricRecorder implements the MetricsRecorder interface 109 // and writes metrics to a spool directory for store-and-forward. 110 type JSONMetricRecorder struct { 111 spoolDir string 112 validMetrics map[string]corecharm.Metric 113 charmURL string 114 uuid utils.UUID 115 created time.Time 116 unitTag string 117 118 lock sync.Mutex 119 120 file io.Closer 121 enc *json.Encoder 122 } 123 124 // MetricRecorderConfig stores configuration data for a metrics recorder. 125 type MetricRecorderConfig struct { 126 SpoolDir string 127 Metrics map[string]corecharm.Metric 128 CharmURL string 129 UnitTag string 130 } 131 132 // NewJSONMetricRecorder creates a new JSON metrics recorder. 133 func NewJSONMetricRecorder(config MetricRecorderConfig) (rec *JSONMetricRecorder, rErr error) { 134 mbUUID, err := utils.NewUUID() 135 if err != nil { 136 return nil, errors.Trace(err) 137 } 138 139 recorder := &JSONMetricRecorder{ 140 spoolDir: config.SpoolDir, 141 uuid: mbUUID, 142 charmURL: config.CharmURL, 143 // TODO(fwereade): 2016-03-17 lp:1558657 144 created: time.Now().UTC(), 145 validMetrics: config.Metrics, 146 unitTag: config.UnitTag, 147 } 148 if err := recorder.open(); err != nil { 149 return nil, errors.Trace(err) 150 } 151 return recorder, nil 152 } 153 154 // Close implements the MetricsRecorder interface. 155 func (m *JSONMetricRecorder) Close() error { 156 m.lock.Lock() 157 defer m.lock.Unlock() 158 159 err := m.file.Close() 160 if err != nil { 161 return errors.Trace(err) 162 } 163 164 // We have an exclusive lock on this metric batch here, because 165 // metricsFile.Close was able to rename the final filename atomically. 166 // 167 // Now write the meta file so that JSONMetricReader discovers a finished 168 // pair of files. 169 err = m.recordMetaData() 170 if err != nil { 171 return errors.Trace(err) 172 } 173 174 return nil 175 } 176 177 // AddMetric implements the MetricsRecorder interface. 178 func (m *JSONMetricRecorder) AddMetric(key, value string, created time.Time) error { 179 err := m.validateMetric(key, value) 180 if err != nil { 181 return errors.Trace(err) 182 } 183 m.lock.Lock() 184 defer m.lock.Unlock() 185 return errors.Trace(m.enc.Encode(jujuc.Metric{Key: key, Value: value, Time: created})) 186 } 187 188 func (m *JSONMetricRecorder) validateMetric(key, value string) error { 189 if !m.IsDeclaredMetric(key) { 190 return errors.Errorf("metric key %q not declared by the charm", key) 191 } 192 // The largest number of digits that can be returned by strconv.FormatFloat is 24, so 193 // choose an arbitrary limit somewhat higher than that. 194 if len(value) > 30 { 195 return fmt.Errorf("metric value is too large") 196 } 197 fValue, err := strconv.ParseFloat(value, 64) 198 if err != nil { 199 return fmt.Errorf("invalid value type: expected float, got %q", value) 200 } 201 if fValue < 0 { 202 return fmt.Errorf("invalid value: value must be greater or equal to zero, got %v", value) 203 } 204 return nil 205 } 206 207 // IsDeclaredMetric returns true if the metric recorder is permitted to store this metric. 208 // Returns false if the uniter using this recorder doesn't define this metric. 209 func (m *JSONMetricRecorder) IsDeclaredMetric(key string) bool { 210 _, ok := m.validMetrics[key] 211 return ok 212 } 213 214 func (m *JSONMetricRecorder) open() error { 215 dataFile := filepath.Join(m.spoolDir, m.uuid.String()) 216 if _, err := os.Stat(dataFile); err != nil && !os.IsNotExist(err) { 217 if err != nil { 218 return errors.Annotatef(err, "failed to stat file %s", dataFile) 219 } 220 return errors.Errorf("file %s already exists", dataFile) 221 } 222 223 dataWriter, err := createMetricFile(dataFile) 224 if err != nil { 225 return errors.Trace(err) 226 } 227 m.file = dataWriter 228 m.enc = json.NewEncoder(dataWriter) 229 return nil 230 } 231 232 func checkSpoolDir(path string) error { 233 if _, err := os.Stat(path); os.IsNotExist(err) { 234 err := os.MkdirAll(path, 0755) 235 if err != nil { 236 return errors.Trace(err) 237 } 238 } else if err != nil { 239 return errors.Trace(err) 240 } 241 return nil 242 } 243 244 func (m *JSONMetricRecorder) recordMetaData() error { 245 metaFile := filepath.Join(m.spoolDir, fmt.Sprintf("%s.meta", m.uuid.String())) 246 if _, err := os.Stat(metaFile); !os.IsNotExist(err) { 247 if err != nil { 248 return errors.Annotatef(err, "failed to stat file %s", metaFile) 249 } 250 return errors.Errorf("file %s already exists", metaFile) 251 } 252 253 metadata := MetricMetadata{ 254 CharmURL: m.charmURL, 255 UUID: m.uuid.String(), 256 Created: m.created, 257 UnitTag: m.unitTag, 258 } 259 // The use of a metricFile here ensures that the JSONMetricReader will only 260 // find a fully-written metafile. 261 metaWriter, err := createMetricFile(metaFile) 262 if err != nil { 263 return errors.Trace(err) 264 } 265 defer metaWriter.Close() 266 enc := json.NewEncoder(metaWriter) 267 err = enc.Encode(metadata) 268 if err != nil { 269 return errors.Trace(err) 270 } 271 return nil 272 } 273 274 // JSONMetricsReader reads metrics batches stored in the spool directory. 275 type JSONMetricReader struct { 276 dir string 277 } 278 279 // NewJSONMetricsReader creates a new JSON metrics reader for the specified spool directory. 280 func NewJSONMetricReader(spoolDir string) (*JSONMetricReader, error) { 281 if _, err := os.Stat(spoolDir); err != nil { 282 return nil, errors.Annotatef(err, "failed to open spool directory %q", spoolDir) 283 } 284 return &JSONMetricReader{ 285 dir: spoolDir, 286 }, nil 287 } 288 289 // Read implements the MetricsReader interface. 290 // Due to the way the batches are stored in the file system, 291 // they will be returned in an arbitrary order. This does not affect the behavior. 292 func (r *JSONMetricReader) Read() ([]MetricBatch, error) { 293 var batches []MetricBatch 294 295 walker := func(path string, info os.FileInfo, err error) error { 296 if err != nil { 297 return errors.Trace(err) 298 } 299 if info.IsDir() && path != r.dir { 300 return filepath.SkipDir 301 } else if !strings.HasSuffix(info.Name(), ".meta") { 302 return nil 303 } 304 305 batch, err := decodeBatch(path) 306 if err != nil { 307 return errors.Trace(err) 308 } 309 batch.Metrics, err = decodeMetrics(filepath.Join(r.dir, batch.UUID)) 310 if err != nil { 311 return errors.Trace(err) 312 } 313 if len(batch.Metrics) > 0 { 314 batches = append(batches, batch) 315 } 316 return nil 317 } 318 if err := filepath.Walk(r.dir, walker); err != nil { 319 return nil, errors.Trace(err) 320 } 321 return batches, nil 322 } 323 324 // Remove implements the MetricsReader interface. 325 func (r *JSONMetricReader) Remove(uuid string) error { 326 metaFile := filepath.Join(r.dir, fmt.Sprintf("%s.meta", uuid)) 327 dataFile := filepath.Join(r.dir, uuid) 328 err := os.Remove(metaFile) 329 if err != nil && !os.IsNotExist(err) { 330 return errors.Trace(err) 331 } 332 err = os.Remove(dataFile) 333 if err != nil { 334 return errors.Trace(err) 335 } 336 return nil 337 } 338 339 // Close implements the MetricsReader interface. 340 func (r *JSONMetricReader) Close() error { 341 return nil 342 } 343 344 func decodeBatch(file string) (MetricBatch, error) { 345 var batch MetricBatch 346 f, err := os.Open(file) 347 if err != nil { 348 return MetricBatch{}, errors.Trace(err) 349 } 350 defer f.Close() 351 dec := json.NewDecoder(f) 352 err = dec.Decode(&batch) 353 if err != nil { 354 return MetricBatch{}, errors.Trace(err) 355 } 356 return batch, nil 357 } 358 359 func decodeMetrics(file string) ([]jujuc.Metric, error) { 360 var metrics []jujuc.Metric 361 f, err := os.Open(file) 362 if err != nil { 363 return nil, errors.Trace(err) 364 } 365 defer f.Close() 366 dec := json.NewDecoder(f) 367 for { 368 var metric jujuc.Metric 369 err := dec.Decode(&metric) 370 if err == io.EOF { 371 break 372 } else if err != nil { 373 return nil, errors.Trace(err) 374 } 375 metrics = append(metrics, metric) 376 } 377 return metrics, nil 378 }