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