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