github.com/hashicorp/go-metrics@v0.5.3/inmem.go (about) 1 package metrics 2 3 import ( 4 "bytes" 5 "fmt" 6 "math" 7 "net/url" 8 "strings" 9 "sync" 10 "time" 11 ) 12 13 var spaceReplacer = strings.NewReplacer(" ", "_") 14 15 // InmemSink provides a MetricSink that does in-memory aggregation 16 // without sending metrics over a network. It can be embedded within 17 // an application to provide profiling information. 18 type InmemSink struct { 19 // How long is each aggregation interval 20 interval time.Duration 21 22 // Retain controls how many metrics interval we keep 23 retain time.Duration 24 25 // maxIntervals is the maximum length of intervals. 26 // It is retain / interval. 27 maxIntervals int 28 29 // intervals is a slice of the retained intervals 30 intervals []*IntervalMetrics 31 intervalLock sync.RWMutex 32 33 rateDenom float64 34 } 35 36 // IntervalMetrics stores the aggregated metrics 37 // for a specific interval 38 type IntervalMetrics struct { 39 sync.RWMutex 40 41 // The start time of the interval 42 Interval time.Time 43 44 // Gauges maps the key to the last set value 45 Gauges map[string]GaugeValue 46 47 // PrecisionGauges maps the key to the last set value 48 PrecisionGauges map[string]PrecisionGaugeValue 49 50 // Points maps the string to the list of emitted values 51 // from EmitKey 52 Points map[string][]float32 53 54 // Counters maps the string key to a sum of the counter 55 // values 56 Counters map[string]SampledValue 57 58 // Samples maps the key to an AggregateSample, 59 // which has the rolled up view of a sample 60 Samples map[string]SampledValue 61 62 // done is closed when this interval has ended, and a new IntervalMetrics 63 // has been created to receive any future metrics. 64 done chan struct{} 65 } 66 67 // NewIntervalMetrics creates a new IntervalMetrics for a given interval 68 func NewIntervalMetrics(intv time.Time) *IntervalMetrics { 69 return &IntervalMetrics{ 70 Interval: intv, 71 Gauges: make(map[string]GaugeValue), 72 PrecisionGauges: make(map[string]PrecisionGaugeValue), 73 Points: make(map[string][]float32), 74 Counters: make(map[string]SampledValue), 75 Samples: make(map[string]SampledValue), 76 done: make(chan struct{}), 77 } 78 } 79 80 // AggregateSample is used to hold aggregate metrics 81 // about a sample 82 type AggregateSample struct { 83 Count int // The count of emitted pairs 84 Rate float64 // The values rate per time unit (usually 1 second) 85 Sum float64 // The sum of values 86 SumSq float64 `json:"-"` // The sum of squared values 87 Min float64 // Minimum value 88 Max float64 // Maximum value 89 LastUpdated time.Time `json:"-"` // When value was last updated 90 } 91 92 // Computes a Stddev of the values 93 func (a *AggregateSample) Stddev() float64 { 94 num := (float64(a.Count) * a.SumSq) - math.Pow(a.Sum, 2) 95 div := float64(a.Count * (a.Count - 1)) 96 if div == 0 { 97 return 0 98 } 99 return math.Sqrt(num / div) 100 } 101 102 // Computes a mean of the values 103 func (a *AggregateSample) Mean() float64 { 104 if a.Count == 0 { 105 return 0 106 } 107 return a.Sum / float64(a.Count) 108 } 109 110 // Ingest is used to update a sample 111 func (a *AggregateSample) Ingest(v float64, rateDenom float64) { 112 a.Count++ 113 a.Sum += v 114 a.SumSq += (v * v) 115 if v < a.Min || a.Count == 1 { 116 a.Min = v 117 } 118 if v > a.Max || a.Count == 1 { 119 a.Max = v 120 } 121 a.Rate = float64(a.Sum) / rateDenom 122 a.LastUpdated = time.Now() 123 } 124 125 func (a *AggregateSample) String() string { 126 if a.Count == 0 { 127 return "Count: 0" 128 } else if a.Stddev() == 0 { 129 return fmt.Sprintf("Count: %d Sum: %0.3f LastUpdated: %s", a.Count, a.Sum, a.LastUpdated) 130 } else { 131 return fmt.Sprintf("Count: %d Min: %0.3f Mean: %0.3f Max: %0.3f Stddev: %0.3f Sum: %0.3f LastUpdated: %s", 132 a.Count, a.Min, a.Mean(), a.Max, a.Stddev(), a.Sum, a.LastUpdated) 133 } 134 } 135 136 // NewInmemSinkFromURL creates an InmemSink from a URL. It is used 137 // (and tested) from NewMetricSinkFromURL. 138 func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) { 139 params := u.Query() 140 141 interval, err := time.ParseDuration(params.Get("interval")) 142 if err != nil { 143 return nil, fmt.Errorf("Bad 'interval' param: %s", err) 144 } 145 146 retain, err := time.ParseDuration(params.Get("retain")) 147 if err != nil { 148 return nil, fmt.Errorf("Bad 'retain' param: %s", err) 149 } 150 151 return NewInmemSink(interval, retain), nil 152 } 153 154 // NewInmemSink is used to construct a new in-memory sink. 155 // Uses an aggregation interval and maximum retention period. 156 func NewInmemSink(interval, retain time.Duration) *InmemSink { 157 rateTimeUnit := time.Second 158 i := &InmemSink{ 159 interval: interval, 160 retain: retain, 161 maxIntervals: int(retain / interval), 162 rateDenom: float64(interval.Nanoseconds()) / float64(rateTimeUnit.Nanoseconds()), 163 } 164 i.intervals = make([]*IntervalMetrics, 0, i.maxIntervals) 165 return i 166 } 167 168 func (i *InmemSink) SetGauge(key []string, val float32) { 169 i.SetGaugeWithLabels(key, val, nil) 170 } 171 172 func (i *InmemSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { 173 k, name := i.flattenKeyLabels(key, labels) 174 intv := i.getInterval() 175 176 intv.Lock() 177 defer intv.Unlock() 178 intv.Gauges[k] = GaugeValue{Name: name, Value: val, Labels: labels} 179 } 180 181 func (i *InmemSink) SetPrecisionGauge(key []string, val float64) { 182 i.SetPrecisionGaugeWithLabels(key, val, nil) 183 } 184 185 func (i *InmemSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 186 k, name := i.flattenKeyLabels(key, labels) 187 intv := i.getInterval() 188 189 intv.Lock() 190 defer intv.Unlock() 191 intv.PrecisionGauges[k] = PrecisionGaugeValue{Name: name, Value: val, Labels: labels} 192 } 193 194 func (i *InmemSink) EmitKey(key []string, val float32) { 195 k := i.flattenKey(key) 196 intv := i.getInterval() 197 198 intv.Lock() 199 defer intv.Unlock() 200 vals := intv.Points[k] 201 intv.Points[k] = append(vals, val) 202 } 203 204 func (i *InmemSink) IncrCounter(key []string, val float32) { 205 i.IncrCounterWithLabels(key, val, nil) 206 } 207 208 func (i *InmemSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { 209 k, name := i.flattenKeyLabels(key, labels) 210 intv := i.getInterval() 211 212 intv.Lock() 213 defer intv.Unlock() 214 215 agg, ok := intv.Counters[k] 216 if !ok { 217 agg = SampledValue{ 218 Name: name, 219 AggregateSample: &AggregateSample{}, 220 Labels: labels, 221 } 222 intv.Counters[k] = agg 223 } 224 agg.Ingest(float64(val), i.rateDenom) 225 } 226 227 func (i *InmemSink) AddSample(key []string, val float32) { 228 i.AddSampleWithLabels(key, val, nil) 229 } 230 231 func (i *InmemSink) AddSampleWithLabels(key []string, val float32, labels []Label) { 232 k, name := i.flattenKeyLabels(key, labels) 233 intv := i.getInterval() 234 235 intv.Lock() 236 defer intv.Unlock() 237 238 agg, ok := intv.Samples[k] 239 if !ok { 240 agg = SampledValue{ 241 Name: name, 242 AggregateSample: &AggregateSample{}, 243 Labels: labels, 244 } 245 intv.Samples[k] = agg 246 } 247 agg.Ingest(float64(val), i.rateDenom) 248 } 249 250 // Data is used to retrieve all the aggregated metrics 251 // Intervals may be in use, and a read lock should be acquired 252 func (i *InmemSink) Data() []*IntervalMetrics { 253 // Get the current interval, forces creation 254 i.getInterval() 255 256 i.intervalLock.RLock() 257 defer i.intervalLock.RUnlock() 258 259 n := len(i.intervals) 260 intervals := make([]*IntervalMetrics, n) 261 262 copy(intervals[:n-1], i.intervals[:n-1]) 263 current := i.intervals[n-1] 264 265 // make its own copy for current interval 266 intervals[n-1] = &IntervalMetrics{} 267 copyCurrent := intervals[n-1] 268 current.RLock() 269 *copyCurrent = *current 270 // RWMutex is not safe to copy, so create a new instance on the copy 271 copyCurrent.RWMutex = sync.RWMutex{} 272 273 copyCurrent.Gauges = make(map[string]GaugeValue, len(current.Gauges)) 274 for k, v := range current.Gauges { 275 copyCurrent.Gauges[k] = v 276 } 277 copyCurrent.PrecisionGauges = make(map[string]PrecisionGaugeValue, len(current.PrecisionGauges)) 278 for k, v := range current.PrecisionGauges { 279 copyCurrent.PrecisionGauges[k] = v 280 } 281 // saved values will be not change, just copy its link 282 copyCurrent.Points = make(map[string][]float32, len(current.Points)) 283 for k, v := range current.Points { 284 copyCurrent.Points[k] = v 285 } 286 copyCurrent.Counters = make(map[string]SampledValue, len(current.Counters)) 287 for k, v := range current.Counters { 288 copyCurrent.Counters[k] = v.deepCopy() 289 } 290 copyCurrent.Samples = make(map[string]SampledValue, len(current.Samples)) 291 for k, v := range current.Samples { 292 copyCurrent.Samples[k] = v.deepCopy() 293 } 294 current.RUnlock() 295 296 return intervals 297 } 298 299 // getInterval returns the current interval. A new interval is created if no 300 // previous interval exists, or if the current time is beyond the window for the 301 // current interval. 302 func (i *InmemSink) getInterval() *IntervalMetrics { 303 intv := time.Now().Truncate(i.interval) 304 305 // Attempt to return the existing interval first, because it only requires 306 // a read lock. 307 i.intervalLock.RLock() 308 n := len(i.intervals) 309 if n > 0 && i.intervals[n-1].Interval == intv { 310 defer i.intervalLock.RUnlock() 311 return i.intervals[n-1] 312 } 313 i.intervalLock.RUnlock() 314 315 i.intervalLock.Lock() 316 defer i.intervalLock.Unlock() 317 318 // Re-check for an existing interval now that the lock is re-acquired. 319 n = len(i.intervals) 320 if n > 0 && i.intervals[n-1].Interval == intv { 321 return i.intervals[n-1] 322 } 323 324 current := NewIntervalMetrics(intv) 325 i.intervals = append(i.intervals, current) 326 if n > 0 { 327 close(i.intervals[n-1].done) 328 } 329 330 n++ 331 // Prune old intervals if the count exceeds the max. 332 if n >= i.maxIntervals { 333 copy(i.intervals[0:], i.intervals[n-i.maxIntervals:]) 334 i.intervals = i.intervals[:i.maxIntervals] 335 } 336 return current 337 } 338 339 // Flattens the key for formatting, removes spaces 340 func (i *InmemSink) flattenKey(parts []string) string { 341 buf := &bytes.Buffer{} 342 343 joined := strings.Join(parts, ".") 344 345 spaceReplacer.WriteString(buf, joined) 346 347 return buf.String() 348 } 349 350 // Flattens the key for formatting along with its labels, removes spaces 351 func (i *InmemSink) flattenKeyLabels(parts []string, labels []Label) (string, string) { 352 key := i.flattenKey(parts) 353 buf := bytes.NewBufferString(key) 354 355 for _, label := range labels { 356 spaceReplacer.WriteString(buf, fmt.Sprintf(";%s=%s", label.Name, label.Value)) 357 } 358 359 return buf.String(), key 360 }