github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/worker/uniter/runner/metrics.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package runner 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/utils" 18 "github.com/juju/utils/fslock" 19 20 "github.com/juju/juju/worker/uniter/runner/jujuc" 21 ) 22 23 const spoolLockName string = "access" 24 25 var lockTimeout = time.Second * 5 26 27 // MetricsMetadata is used to store metadata for the current metric batch. 28 type MetricsMetadata struct { 29 CharmURL string `json:"charmurl"` 30 UUID string `json:"uuid"` 31 Created time.Time `json:"created"` 32 } 33 34 // JSONMetricsRecorder implements the MetricsRecorder interface 35 // and writes metrics to a spool directory for store-and-forward. 36 type JSONMetricsRecorder struct { 37 sync.Mutex 38 39 path string 40 41 file io.Closer 42 enc *json.Encoder 43 } 44 45 // NewJSONMetricsRecorder creates a new JSON metrics recorder. 46 // It checks if the metrics spool directory exists, if it does not - it is created. Then 47 // it tries to find an unused metric batch UUID 3 times. 48 func NewJSONMetricsRecorder(spoolDir string, charmURL string) (rec *JSONMetricsRecorder, rErr error) { 49 lock, err := fslock.NewLock(spoolDir, spoolLockName) 50 if err != nil { 51 return nil, errors.Trace(err) 52 } 53 if err := lock.LockWithTimeout(lockTimeout, "initializing recorder"); err != nil { 54 return nil, errors.Trace(err) 55 } 56 defer func() { 57 err := lock.Unlock() 58 if err != nil && rErr == nil { 59 rErr = errors.Trace(err) 60 rec = nil 61 } else if err != nil { 62 rErr = errors.Annotatef(err, "failed to unlock spool directory %q", spoolDir) 63 } 64 }() 65 66 if err := checkSpoolDir(spoolDir); err != nil { 67 return nil, errors.Trace(err) 68 } 69 70 mbUUID, err := utils.NewUUID() 71 if err != nil { 72 return nil, errors.Trace(err) 73 } 74 75 metaFile := filepath.Join(spoolDir, fmt.Sprintf("%s.meta", mbUUID.String())) 76 dataFile := filepath.Join(spoolDir, mbUUID.String()) 77 if _, err := os.Stat(metaFile); !os.IsNotExist(err) { 78 if err != nil { 79 return nil, errors.Annotatef(err, "failed to stat file %s", metaFile) 80 } 81 return nil, errors.Errorf("file %s already exists", metaFile) 82 } 83 if _, err := os.Stat(dataFile); err != nil && !os.IsNotExist(err) { 84 if err != nil { 85 return nil, errors.Annotatef(err, "failed to stat file %s", dataFile) 86 } 87 return nil, errors.Errorf("file %s already exists", dataFile) 88 } 89 90 if err := recordMetaData(metaFile, charmURL, mbUUID.String()); err != nil { 91 return nil, errors.Trace(err) 92 } 93 94 recorder := &JSONMetricsRecorder{ 95 path: dataFile, 96 } 97 if err := recorder.open(); err != nil { 98 return nil, errors.Trace(err) 99 } 100 return recorder, nil 101 } 102 103 // Close implements the MetricsRecorder interface. 104 func (m *JSONMetricsRecorder) Close() error { 105 m.Lock() 106 defer m.Unlock() 107 return errors.Trace(m.file.Close()) 108 } 109 110 // AddMetric implements the MetricsRecorder interface. 111 func (m *JSONMetricsRecorder) AddMetric(key, value string, created time.Time) error { 112 m.Lock() 113 defer m.Unlock() 114 return errors.Trace(m.enc.Encode(jujuc.Metric{Key: key, Value: value, Time: created})) 115 } 116 117 func (m *JSONMetricsRecorder) open() error { 118 dataWriter, err := os.Create(m.path) 119 if err != nil { 120 return errors.Trace(err) 121 } 122 m.file = dataWriter 123 m.enc = json.NewEncoder(dataWriter) 124 return nil 125 126 } 127 128 func checkSpoolDir(path string) error { 129 if _, err := os.Stat(path); os.IsNotExist(err) { 130 err := os.MkdirAll(path, 0755) 131 if err != nil { 132 return errors.Trace(err) 133 } 134 } else if err != nil { 135 return errors.Trace(err) 136 } 137 return nil 138 } 139 140 func recordMetaData(path string, charmURL, UUID string) error { 141 metadata := MetricsMetadata{ 142 CharmURL: charmURL, 143 UUID: UUID, 144 Created: time.Now().UTC(), 145 } 146 metaWriter, err := os.Create(path) 147 if err != nil { 148 return errors.Trace(err) 149 } 150 defer metaWriter.Close() 151 enc := json.NewEncoder(metaWriter) 152 err = enc.Encode(metadata) 153 if err != nil { 154 return errors.Trace(err) 155 } 156 return nil 157 } 158 159 // MetricsBatch stores the information relevant to a single metrics batch. 160 type MetricsBatch struct { 161 CharmURL string `json:"charmurl"` 162 UUID string `json:"uuid"` 163 Created time.Time `json:"created"` 164 Metrics []jujuc.Metric `json:"metrics"` 165 } 166 167 // JSONMetricsReader reads metrics batches stored in the spool directory. 168 type JSONMetricsReader struct { 169 dir string 170 lock *fslock.Lock 171 } 172 173 // NewJSONMetricsReader creates a new JSON metrics reader for the specified spool directory. 174 func NewJSONMetricsReader(spoolDir string) (*JSONMetricsReader, error) { 175 if _, err := os.Stat(spoolDir); err != nil { 176 return nil, errors.Annotatef(err, "failed to open spool directory %q", spoolDir) 177 } 178 lock, err := fslock.NewLock(spoolDir, spoolLockName) 179 if err != nil { 180 return nil, errors.Trace(err) 181 } 182 return &JSONMetricsReader{ 183 lock: lock, 184 dir: spoolDir, 185 }, nil 186 } 187 188 // Open implements the MetricsReader interface. 189 // Due to the way the batches are stored in the file system, 190 // they will be returned in an arbitrary order. This does not affect the behavior. 191 func (r *JSONMetricsReader) Open() ([]MetricsBatch, error) { 192 var batches []MetricsBatch 193 194 if err := r.lock.LockWithTimeout(lockTimeout, "reading"); err != nil { 195 return nil, errors.Trace(err) 196 } 197 198 walker := func(path string, info os.FileInfo, err error) error { 199 if err != nil { 200 return errors.Trace(err) 201 } 202 if info.IsDir() && path != r.dir { 203 return filepath.SkipDir 204 } else if !strings.HasSuffix(info.Name(), ".meta") { 205 return nil 206 } 207 208 batch, err := decodeBatch(path) 209 if err != nil { 210 return errors.Trace(err) 211 } 212 batch.Metrics, err = decodeMetrics(filepath.Join(r.dir, batch.UUID)) 213 if err != nil { 214 return errors.Trace(err) 215 } 216 batches = append(batches, batch) 217 return nil 218 } 219 if err := filepath.Walk(r.dir, walker); err != nil { 220 return nil, errors.Trace(err) 221 } 222 return batches, nil 223 } 224 225 // Remove implements the MetricsReader interface. 226 func (r *JSONMetricsReader) Remove(uuid string) error { 227 metaFile := filepath.Join(r.dir, fmt.Sprintf("%s.meta", uuid)) 228 dataFile := filepath.Join(r.dir, uuid) 229 err := os.Remove(metaFile) 230 if err != nil && !os.IsNotExist(err) { 231 return errors.Trace(err) 232 } 233 err = os.Remove(dataFile) 234 if err != nil { 235 return errors.Trace(err) 236 } 237 return nil 238 } 239 240 // Close implements the MetricsReader interface. 241 func (r *JSONMetricsReader) Close() error { 242 if r.lock.IsLockHeld() { 243 return r.lock.Unlock() 244 } 245 return nil 246 } 247 248 func decodeBatch(file string) (MetricsBatch, error) { 249 var batch MetricsBatch 250 f, err := os.Open(file) 251 if err != nil { 252 return MetricsBatch{}, errors.Trace(err) 253 } 254 defer f.Close() 255 dec := json.NewDecoder(f) 256 err = dec.Decode(&batch) 257 if err != nil { 258 return MetricsBatch{}, errors.Trace(err) 259 } 260 return batch, nil 261 } 262 263 func decodeMetrics(file string) ([]jujuc.Metric, error) { 264 var metrics []jujuc.Metric 265 f, err := os.Open(file) 266 if err != nil { 267 return nil, errors.Trace(err) 268 } 269 defer f.Close() 270 dec := json.NewDecoder(f) 271 for { 272 var metric jujuc.Metric 273 err := dec.Decode(&metric) 274 if err == io.EOF { 275 break 276 } else if err != nil { 277 return nil, errors.Trace(err) 278 } 279 metrics = append(metrics, metric) 280 } 281 return metrics, nil 282 }