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  }