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  }