github.com/newrelic/go-agent@v3.26.0+incompatible/internal/metrics.go (about)

     1  // Copyright 2020 New Relic Corporation. All rights reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package internal
     5  
     6  import (
     7  	"bytes"
     8  	"time"
     9  
    10  	"github.com/newrelic/go-agent/internal/jsonx"
    11  )
    12  
    13  type metricForce int
    14  
    15  const (
    16  	forced metricForce = iota
    17  	unforced
    18  )
    19  
    20  type metricID struct {
    21  	Name  string `json:"name"`
    22  	Scope string `json:"scope,omitempty"`
    23  }
    24  
    25  type metricData struct {
    26  	// These values are in the units expected by the collector.
    27  	countSatisfied  float64 // Seconds, or count for Apdex
    28  	totalTolerated  float64 // Seconds, or count for Apdex
    29  	exclusiveFailed float64 // Seconds, or count for Apdex
    30  	min             float64 // Seconds
    31  	max             float64 // Seconds
    32  	sumSquares      float64 // Seconds**2, or 0 for Apdex
    33  }
    34  
    35  func metricDataFromDuration(duration, exclusive time.Duration) metricData {
    36  	ds := duration.Seconds()
    37  	return metricData{
    38  		countSatisfied:  1,
    39  		totalTolerated:  ds,
    40  		exclusiveFailed: exclusive.Seconds(),
    41  		min:             ds,
    42  		max:             ds,
    43  		sumSquares:      ds * ds,
    44  	}
    45  }
    46  
    47  type metric struct {
    48  	forced metricForce
    49  	data   metricData
    50  }
    51  
    52  type metricTable struct {
    53  	metricPeriodStart time.Time
    54  	failedHarvests    int
    55  	maxTableSize      int // After this max is reached, only forced metrics are added
    56  	metrics           map[metricID]*metric
    57  }
    58  
    59  func newMetricTable(maxTableSize int, now time.Time) *metricTable {
    60  	return &metricTable{
    61  		metricPeriodStart: now,
    62  		metrics:           make(map[metricID]*metric),
    63  		maxTableSize:      maxTableSize,
    64  		failedHarvests:    0,
    65  	}
    66  }
    67  
    68  func (mt *metricTable) full() bool {
    69  	return len(mt.metrics) >= mt.maxTableSize
    70  }
    71  
    72  func (data *metricData) aggregate(src metricData) {
    73  	data.countSatisfied += src.countSatisfied
    74  	data.totalTolerated += src.totalTolerated
    75  	data.exclusiveFailed += src.exclusiveFailed
    76  
    77  	if src.min < data.min {
    78  		data.min = src.min
    79  	}
    80  	if src.max > data.max {
    81  		data.max = src.max
    82  	}
    83  
    84  	data.sumSquares += src.sumSquares
    85  }
    86  
    87  func (mt *metricTable) mergeMetric(id metricID, m metric) {
    88  	if to := mt.metrics[id]; nil != to {
    89  		to.data.aggregate(m.data)
    90  		return
    91  	}
    92  
    93  	if mt.full() && (unforced == m.forced) {
    94  		mt.addSingleCount(supportabilityDropped, forced)
    95  		return
    96  	}
    97  	// NOTE: `new` is used in place of `&m` since the latter will make `m`
    98  	// get heap allocated regardless of whether or not this line gets
    99  	// reached (running go version go1.5 darwin/amd64).  See
   100  	// BenchmarkAddingSameMetrics.
   101  	alloc := new(metric)
   102  	*alloc = m
   103  	mt.metrics[id] = alloc
   104  }
   105  
   106  func (mt *metricTable) mergeFailed(from *metricTable) {
   107  	fails := from.failedHarvests + 1
   108  	if fails >= failedMetricAttemptsLimit {
   109  		return
   110  	}
   111  	if from.metricPeriodStart.Before(mt.metricPeriodStart) {
   112  		mt.metricPeriodStart = from.metricPeriodStart
   113  	}
   114  	mt.failedHarvests = fails
   115  	mt.merge(from, "")
   116  }
   117  
   118  func (mt *metricTable) merge(from *metricTable, newScope string) {
   119  	if "" == newScope {
   120  		for id, m := range from.metrics {
   121  			mt.mergeMetric(id, *m)
   122  		}
   123  	} else {
   124  		for id, m := range from.metrics {
   125  			mt.mergeMetric(metricID{Name: id.Name, Scope: newScope}, *m)
   126  		}
   127  	}
   128  }
   129  
   130  func (mt *metricTable) add(name, scope string, data metricData, force metricForce) {
   131  	mt.mergeMetric(metricID{Name: name, Scope: scope}, metric{data: data, forced: force})
   132  }
   133  
   134  func (mt *metricTable) addCount(name string, count float64, force metricForce) {
   135  	mt.add(name, "", metricData{countSatisfied: count}, force)
   136  }
   137  
   138  func (mt *metricTable) addSingleCount(name string, force metricForce) {
   139  	mt.addCount(name, float64(1), force)
   140  }
   141  
   142  func (mt *metricTable) addDuration(name, scope string, duration, exclusive time.Duration, force metricForce) {
   143  	mt.add(name, scope, metricDataFromDuration(duration, exclusive), force)
   144  }
   145  
   146  func (mt *metricTable) addValueExclusive(name, scope string, total, exclusive float64, force metricForce) {
   147  	data := metricData{
   148  		countSatisfied:  1,
   149  		totalTolerated:  total,
   150  		exclusiveFailed: exclusive,
   151  		min:             total,
   152  		max:             total,
   153  		sumSquares:      total * total,
   154  	}
   155  	mt.add(name, scope, data, force)
   156  }
   157  
   158  func (mt *metricTable) addValue(name, scope string, total float64, force metricForce) {
   159  	mt.addValueExclusive(name, scope, total, total, force)
   160  }
   161  
   162  func (mt *metricTable) addApdex(name, scope string, apdexThreshold time.Duration, zone ApdexZone, force metricForce) {
   163  	apdexSeconds := apdexThreshold.Seconds()
   164  	data := metricData{min: apdexSeconds, max: apdexSeconds}
   165  
   166  	switch zone {
   167  	case ApdexSatisfying:
   168  		data.countSatisfied = 1
   169  	case ApdexTolerating:
   170  		data.totalTolerated = 1
   171  	case ApdexFailing:
   172  		data.exclusiveFailed = 1
   173  	}
   174  
   175  	mt.add(name, scope, data, force)
   176  }
   177  
   178  func (mt *metricTable) CollectorJSON(agentRunID string, now time.Time) ([]byte, error) {
   179  	if 0 == len(mt.metrics) {
   180  		return nil, nil
   181  	}
   182  	estimatedBytesPerMetric := 128
   183  	estimatedLen := len(mt.metrics) * estimatedBytesPerMetric
   184  	buf := bytes.NewBuffer(make([]byte, 0, estimatedLen))
   185  	buf.WriteByte('[')
   186  
   187  	jsonx.AppendString(buf, agentRunID)
   188  	buf.WriteByte(',')
   189  	jsonx.AppendInt(buf, mt.metricPeriodStart.Unix())
   190  	buf.WriteByte(',')
   191  	jsonx.AppendInt(buf, now.Unix())
   192  	buf.WriteByte(',')
   193  
   194  	buf.WriteByte('[')
   195  	first := true
   196  	for id, metric := range mt.metrics {
   197  		if first {
   198  			first = false
   199  		} else {
   200  			buf.WriteByte(',')
   201  		}
   202  		buf.WriteByte('[')
   203  		buf.WriteByte('{')
   204  		buf.WriteString(`"name":`)
   205  		jsonx.AppendString(buf, id.Name)
   206  		if id.Scope != "" {
   207  			buf.WriteString(`,"scope":`)
   208  			jsonx.AppendString(buf, id.Scope)
   209  		}
   210  		buf.WriteByte('}')
   211  		buf.WriteByte(',')
   212  
   213  		jsonx.AppendFloatArray(buf,
   214  			metric.data.countSatisfied,
   215  			metric.data.totalTolerated,
   216  			metric.data.exclusiveFailed,
   217  			metric.data.min,
   218  			metric.data.max,
   219  			metric.data.sumSquares)
   220  
   221  		buf.WriteByte(']')
   222  	}
   223  	buf.WriteByte(']')
   224  
   225  	buf.WriteByte(']')
   226  	return buf.Bytes(), nil
   227  }
   228  
   229  func (mt *metricTable) Data(agentRunID string, harvestStart time.Time) ([]byte, error) {
   230  	return mt.CollectorJSON(agentRunID, harvestStart)
   231  }
   232  func (mt *metricTable) MergeIntoHarvest(h *Harvest) {
   233  	h.Metrics.mergeFailed(mt)
   234  }
   235  
   236  func (mt *metricTable) ApplyRules(rules metricRules) *metricTable {
   237  	if nil == rules {
   238  		return mt
   239  	}
   240  	if len(rules) == 0 {
   241  		return mt
   242  	}
   243  
   244  	applied := newMetricTable(mt.maxTableSize, mt.metricPeriodStart)
   245  	cache := make(map[string]string)
   246  
   247  	for id, m := range mt.metrics {
   248  		out, ok := cache[id.Name]
   249  		if !ok {
   250  			out = rules.Apply(id.Name)
   251  			cache[id.Name] = out
   252  		}
   253  
   254  		if "" != out {
   255  			applied.mergeMetric(metricID{Name: out, Scope: id.Scope}, *m)
   256  		}
   257  	}
   258  
   259  	return applied
   260  }
   261  
   262  func (mt *metricTable) EndpointMethod() string {
   263  	return cmdMetrics
   264  }