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 }