|
@@ -1,8 +1,10 @@
|
|
|
package metrics
|
|
|
|
|
|
import (
|
|
|
+ "bytes"
|
|
|
"fmt"
|
|
|
"math"
|
|
|
+ "net/url"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
"time"
|
|
@@ -25,6 +27,8 @@ type InmemSink struct {
|
|
|
// intervals is a slice of the retained intervals
|
|
|
intervals []*IntervalMetrics
|
|
|
intervalLock sync.RWMutex
|
|
|
+
|
|
|
+ rateDenom float64
|
|
|
}
|
|
|
|
|
|
// IntervalMetrics stores the aggregated metrics
|
|
@@ -36,7 +40,7 @@ type IntervalMetrics struct {
|
|
|
Interval time.Time
|
|
|
|
|
|
// Gauges maps the key to the last set value
|
|
|
- Gauges map[string]float32
|
|
|
+ Gauges map[string]GaugeValue
|
|
|
|
|
|
// Points maps the string to the list of emitted values
|
|
|
// from EmitKey
|
|
@@ -44,32 +48,34 @@ type IntervalMetrics struct {
|
|
|
|
|
|
// Counters maps the string key to a sum of the counter
|
|
|
// values
|
|
|
- Counters map[string]*AggregateSample
|
|
|
+ Counters map[string]SampledValue
|
|
|
|
|
|
// Samples maps the key to an AggregateSample,
|
|
|
// which has the rolled up view of a sample
|
|
|
- Samples map[string]*AggregateSample
|
|
|
+ Samples map[string]SampledValue
|
|
|
}
|
|
|
|
|
|
// NewIntervalMetrics creates a new IntervalMetrics for a given interval
|
|
|
func NewIntervalMetrics(intv time.Time) *IntervalMetrics {
|
|
|
return &IntervalMetrics{
|
|
|
Interval: intv,
|
|
|
- Gauges: make(map[string]float32),
|
|
|
+ Gauges: make(map[string]GaugeValue),
|
|
|
Points: make(map[string][]float32),
|
|
|
- Counters: make(map[string]*AggregateSample),
|
|
|
- Samples: make(map[string]*AggregateSample),
|
|
|
+ Counters: make(map[string]SampledValue),
|
|
|
+ Samples: make(map[string]SampledValue),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// AggregateSample is used to hold aggregate metrics
|
|
|
// about a sample
|
|
|
type AggregateSample struct {
|
|
|
- Count int // The count of emitted pairs
|
|
|
- Sum float64 // The sum of values
|
|
|
- SumSq float64 // The sum of squared values
|
|
|
- Min float64 // Minimum value
|
|
|
- Max float64 // Maximum value
|
|
|
+ Count int // The count of emitted pairs
|
|
|
+ Rate float64 // The values rate per time unit (usually 1 second)
|
|
|
+ Sum float64 // The sum of values
|
|
|
+ SumSq float64 `json:"-"` // The sum of squared values
|
|
|
+ Min float64 // Minimum value
|
|
|
+ Max float64 // Maximum value
|
|
|
+ LastUpdated time.Time `json:"-"` // When value was last updated
|
|
|
}
|
|
|
|
|
|
// Computes a Stddev of the values
|
|
@@ -91,7 +97,7 @@ func (a *AggregateSample) Mean() float64 {
|
|
|
}
|
|
|
|
|
|
// Ingest is used to update a sample
|
|
|
-func (a *AggregateSample) Ingest(v float64) {
|
|
|
+func (a *AggregateSample) Ingest(v float64, rateDenom float64) {
|
|
|
a.Count++
|
|
|
a.Sum += v
|
|
|
a.SumSq += (v * v)
|
|
@@ -101,38 +107,64 @@ func (a *AggregateSample) Ingest(v float64) {
|
|
|
if v > a.Max || a.Count == 1 {
|
|
|
a.Max = v
|
|
|
}
|
|
|
+ a.Rate = float64(a.Sum) / rateDenom
|
|
|
+ a.LastUpdated = time.Now()
|
|
|
}
|
|
|
|
|
|
func (a *AggregateSample) String() string {
|
|
|
if a.Count == 0 {
|
|
|
return "Count: 0"
|
|
|
} else if a.Stddev() == 0 {
|
|
|
- return fmt.Sprintf("Count: %d Sum: %0.3f", a.Count, a.Sum)
|
|
|
+ return fmt.Sprintf("Count: %d Sum: %0.3f LastUpdated: %s", a.Count, a.Sum, a.LastUpdated)
|
|
|
} else {
|
|
|
- return fmt.Sprintf("Count: %d Min: %0.3f Mean: %0.3f Max: %0.3f Stddev: %0.3f Sum: %0.3f",
|
|
|
- a.Count, a.Min, a.Mean(), a.Max, a.Stddev(), a.Sum)
|
|
|
+ return fmt.Sprintf("Count: %d Min: %0.3f Mean: %0.3f Max: %0.3f Stddev: %0.3f Sum: %0.3f LastUpdated: %s",
|
|
|
+ a.Count, a.Min, a.Mean(), a.Max, a.Stddev(), a.Sum, a.LastUpdated)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// NewInmemSinkFromURL creates an InmemSink from a URL. It is used
|
|
|
+// (and tested) from NewMetricSinkFromURL.
|
|
|
+func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) {
|
|
|
+ params := u.Query()
|
|
|
+
|
|
|
+ interval, err := time.ParseDuration(params.Get("interval"))
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("Bad 'interval' param: %s", err)
|
|
|
}
|
|
|
+
|
|
|
+ retain, err := time.ParseDuration(params.Get("retain"))
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("Bad 'retain' param: %s", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ return NewInmemSink(interval, retain), nil
|
|
|
}
|
|
|
|
|
|
// NewInmemSink is used to construct a new in-memory sink.
|
|
|
// Uses an aggregation interval and maximum retention period.
|
|
|
func NewInmemSink(interval, retain time.Duration) *InmemSink {
|
|
|
+ rateTimeUnit := time.Second
|
|
|
i := &InmemSink{
|
|
|
interval: interval,
|
|
|
retain: retain,
|
|
|
maxIntervals: int(retain / interval),
|
|
|
+ rateDenom: float64(interval.Nanoseconds()) / float64(rateTimeUnit.Nanoseconds()),
|
|
|
}
|
|
|
i.intervals = make([]*IntervalMetrics, 0, i.maxIntervals)
|
|
|
return i
|
|
|
}
|
|
|
|
|
|
func (i *InmemSink) SetGauge(key []string, val float32) {
|
|
|
- k := i.flattenKey(key)
|
|
|
+ i.SetGaugeWithLabels(key, val, nil)
|
|
|
+}
|
|
|
+
|
|
|
+func (i *InmemSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {
|
|
|
+ k, name := i.flattenKeyLabels(key, labels)
|
|
|
intv := i.getInterval()
|
|
|
|
|
|
intv.Lock()
|
|
|
defer intv.Unlock()
|
|
|
- intv.Gauges[k] = val
|
|
|
+ intv.Gauges[k] = GaugeValue{Name: name, Value: val, Labels: labels}
|
|
|
}
|
|
|
|
|
|
func (i *InmemSink) EmitKey(key []string, val float32) {
|
|
@@ -146,33 +178,49 @@ func (i *InmemSink) EmitKey(key []string, val float32) {
|
|
|
}
|
|
|
|
|
|
func (i *InmemSink) IncrCounter(key []string, val float32) {
|
|
|
- k := i.flattenKey(key)
|
|
|
+ i.IncrCounterWithLabels(key, val, nil)
|
|
|
+}
|
|
|
+
|
|
|
+func (i *InmemSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {
|
|
|
+ k, name := i.flattenKeyLabels(key, labels)
|
|
|
intv := i.getInterval()
|
|
|
|
|
|
intv.Lock()
|
|
|
defer intv.Unlock()
|
|
|
|
|
|
- agg := intv.Counters[k]
|
|
|
- if agg == nil {
|
|
|
- agg = &AggregateSample{}
|
|
|
+ agg, ok := intv.Counters[k]
|
|
|
+ if !ok {
|
|
|
+ agg = SampledValue{
|
|
|
+ Name: name,
|
|
|
+ AggregateSample: &AggregateSample{},
|
|
|
+ Labels: labels,
|
|
|
+ }
|
|
|
intv.Counters[k] = agg
|
|
|
}
|
|
|
- agg.Ingest(float64(val))
|
|
|
+ agg.Ingest(float64(val), i.rateDenom)
|
|
|
}
|
|
|
|
|
|
func (i *InmemSink) AddSample(key []string, val float32) {
|
|
|
- k := i.flattenKey(key)
|
|
|
+ i.AddSampleWithLabels(key, val, nil)
|
|
|
+}
|
|
|
+
|
|
|
+func (i *InmemSink) AddSampleWithLabels(key []string, val float32, labels []Label) {
|
|
|
+ k, name := i.flattenKeyLabels(key, labels)
|
|
|
intv := i.getInterval()
|
|
|
|
|
|
intv.Lock()
|
|
|
defer intv.Unlock()
|
|
|
|
|
|
- agg := intv.Samples[k]
|
|
|
- if agg == nil {
|
|
|
- agg = &AggregateSample{}
|
|
|
+ agg, ok := intv.Samples[k]
|
|
|
+ if !ok {
|
|
|
+ agg = SampledValue{
|
|
|
+ Name: name,
|
|
|
+ AggregateSample: &AggregateSample{},
|
|
|
+ Labels: labels,
|
|
|
+ }
|
|
|
intv.Samples[k] = agg
|
|
|
}
|
|
|
- agg.Ingest(float64(val))
|
|
|
+ agg.Ingest(float64(val), i.rateDenom)
|
|
|
}
|
|
|
|
|
|
// Data is used to retrieve all the aggregated metrics
|
|
@@ -184,8 +232,37 @@ func (i *InmemSink) Data() []*IntervalMetrics {
|
|
|
i.intervalLock.RLock()
|
|
|
defer i.intervalLock.RUnlock()
|
|
|
|
|
|
- intervals := make([]*IntervalMetrics, len(i.intervals))
|
|
|
- copy(intervals, i.intervals)
|
|
|
+ n := len(i.intervals)
|
|
|
+ intervals := make([]*IntervalMetrics, n)
|
|
|
+
|
|
|
+ copy(intervals[:n-1], i.intervals[:n-1])
|
|
|
+ current := i.intervals[n-1]
|
|
|
+
|
|
|
+ // make its own copy for current interval
|
|
|
+ intervals[n-1] = &IntervalMetrics{}
|
|
|
+ copyCurrent := intervals[n-1]
|
|
|
+ current.RLock()
|
|
|
+ *copyCurrent = *current
|
|
|
+
|
|
|
+ copyCurrent.Gauges = make(map[string]GaugeValue, len(current.Gauges))
|
|
|
+ for k, v := range current.Gauges {
|
|
|
+ copyCurrent.Gauges[k] = v
|
|
|
+ }
|
|
|
+ // saved values will be not change, just copy its link
|
|
|
+ copyCurrent.Points = make(map[string][]float32, len(current.Points))
|
|
|
+ for k, v := range current.Points {
|
|
|
+ copyCurrent.Points[k] = v
|
|
|
+ }
|
|
|
+ copyCurrent.Counters = make(map[string]SampledValue, len(current.Counters))
|
|
|
+ for k, v := range current.Counters {
|
|
|
+ copyCurrent.Counters[k] = v
|
|
|
+ }
|
|
|
+ copyCurrent.Samples = make(map[string]SampledValue, len(current.Samples))
|
|
|
+ for k, v := range current.Samples {
|
|
|
+ copyCurrent.Samples[k] = v
|
|
|
+ }
|
|
|
+ current.RUnlock()
|
|
|
+
|
|
|
return intervals
|
|
|
}
|
|
|
|
|
@@ -234,6 +311,38 @@ func (i *InmemSink) getInterval() *IntervalMetrics {
|
|
|
|
|
|
// Flattens the key for formatting, removes spaces
|
|
|
func (i *InmemSink) flattenKey(parts []string) string {
|
|
|
- joined := strings.Join(parts, ".")
|
|
|
- return strings.Replace(joined, " ", "_", -1)
|
|
|
+ buf := &bytes.Buffer{}
|
|
|
+ replacer := strings.NewReplacer(" ", "_")
|
|
|
+
|
|
|
+ if len(parts) > 0 {
|
|
|
+ replacer.WriteString(buf, parts[0])
|
|
|
+ }
|
|
|
+ for _, part := range parts[1:] {
|
|
|
+ replacer.WriteString(buf, ".")
|
|
|
+ replacer.WriteString(buf, part)
|
|
|
+ }
|
|
|
+
|
|
|
+ return buf.String()
|
|
|
+}
|
|
|
+
|
|
|
+// Flattens the key for formatting along with its labels, removes spaces
|
|
|
+func (i *InmemSink) flattenKeyLabels(parts []string, labels []Label) (string, string) {
|
|
|
+ buf := &bytes.Buffer{}
|
|
|
+ replacer := strings.NewReplacer(" ", "_")
|
|
|
+
|
|
|
+ if len(parts) > 0 {
|
|
|
+ replacer.WriteString(buf, parts[0])
|
|
|
+ }
|
|
|
+ for _, part := range parts[1:] {
|
|
|
+ replacer.WriteString(buf, ".")
|
|
|
+ replacer.WriteString(buf, part)
|
|
|
+ }
|
|
|
+
|
|
|
+ key := buf.String()
|
|
|
+
|
|
|
+ for _, label := range labels {
|
|
|
+ replacer.WriteString(buf, fmt.Sprintf(";%s=%s", label.Name, label.Value))
|
|
|
+ }
|
|
|
+
|
|
|
+ return buf.String(), key
|
|
|
}
|