master
go 281 lines 7.88 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package metrix
4
5 import (
6 "fmt"
7 "math"
8 "sort"
9 "strconv"
10 )
11
12 const HistogramBucketLabel = "le"
13
14 // snapshotHistogramInstrument writes sampled full histogram points.
15 type snapshotHistogramInstrument struct {
16 backend meterBackend
17 desc *instrumentDescriptor
18 scope HostScope
19 base []LabelSet
20 }
21
22 // statefulHistogramInstrument writes observed samples into maintained histogram state.
23 type statefulHistogramInstrument struct {
24 backend meterBackend
25 desc *instrumentDescriptor
26 scope HostScope
27 base []LabelSet
28 }
29
30 // stagedHistogram holds one in-cycle histogram sample for a single series identity.
31 type stagedHistogram struct {
32 key string
33 name string
34 hostScopeKey string
35 hostScope HostScope
36 labels []Label
37 labelsKey string
38 desc *instrumentDescriptor
39 bounds []float64
40 count SampleValue
41 sum SampleValue
42 cumulative []SampleValue
43 }
44
45 // Histogram declares or reuses a snapshot histogram under this meter.
46 func (m *snapshotMeter) Histogram(name string, opts ...InstrumentOption) SnapshotHistogram {
47 desc, err := m.backend.registerInstrument(metricName(m.prefix, name), kindHistogram, modeSnapshot, opts...)
48 if err != nil {
49 panic(err)
50 }
51 return &snapshotHistogramInstrument{
52 backend: m.backend,
53 desc: desc,
54 scope: m.scope,
55 base: appendLabelSets(m.sets, nil),
56 }
57 }
58
59 // Histogram declares or reuses a stateful histogram under this meter.
60 func (m *statefulMeter) Histogram(name string, opts ...InstrumentOption) StatefulHistogram {
61 desc, err := m.backend.registerInstrument(metricName(m.prefix, name), kindHistogram, modeStateful, opts...)
62 if err != nil {
63 panic(err)
64 }
65 return &statefulHistogramInstrument{
66 backend: m.backend,
67 desc: desc,
68 scope: m.scope,
69 base: appendLabelSets(m.sets, nil),
70 }
71 }
72
73 // ObservePoint writes one full histogram point for this collect cycle.
74 func (h *snapshotHistogramInstrument) ObservePoint(p HistogramPoint, labels ...LabelSet) {
75 h.backend.recordHistogramObservePoint(h.desc, h.scope, p, appendLabelSets(h.base, labels))
76 }
77
78 // Observe adds one sample to a stateful histogram for this collect cycle.
79 func (h *statefulHistogramInstrument) Observe(v SampleValue, labels ...LabelSet) {
80 h.backend.recordHistogramObserve(h.desc, h.scope, v, appendLabelSets(h.base, labels))
81 }
82
83 // recordHistogramObservePoint writes one full histogram point into the active frame.
84 func (c *storeCore) recordHistogramObservePoint(desc *instrumentDescriptor, scope HostScope, point HistogramPoint, sets []LabelSet) {
85 c.mu.Lock()
86 defer c.mu.Unlock()
87
88 if c.active == nil {
89 panic(errCycleInactive)
90 }
91
92 labels, labelsKey, err := labelsFromSet(sets, c)
93 if err != nil {
94 panic(err)
95 }
96 if labelsContainKey(labels, HistogramBucketLabel) {
97 panic(errHistogramLabelKey)
98 }
99 scope, ok := c.prepareHostScopeForWriteLocked(scope)
100 if !ok {
101 return
102 }
103
104 schema := desc.histogram
105 if schema == nil {
106 // For snapshot histograms without explicit bounds, validate against
107 // previously captured family schema (if available).
108 schema = c.snapshotHistogramSchema[desc.name]
109 }
110 bounds, count, sum, cumulative := normalizeHistogramPoint(point, schema)
111
112 key := makeSeriesKey(scope.ScopeKey, desc.name, labelsKey)
113 entry, ok := c.active.histograms[key]
114 if !ok {
115 entry = &stagedHistogram{
116 key: key,
117 name: desc.name,
118 hostScopeKey: scope.ScopeKey,
119 hostScope: scope,
120 labels: labels,
121 labelsKey: labelsKey,
122 desc: desc,
123 }
124 c.active.histograms[key] = entry
125 }
126 if len(entry.bounds) > 0 && !equalHistogramBounds(entry.bounds, bounds) {
127 panic("metrix: histogram point schema mismatch within cycle")
128 }
129 entry.bounds = append(entry.bounds[:0], bounds...)
130 entry.count = count
131 entry.sum = sum
132 entry.cumulative = append(entry.cumulative[:0], cumulative...)
133 }
134
135 // recordHistogramObserve adds one sample to a stateful histogram in the active frame.
136 func (c *storeCore) recordHistogramObserve(desc *instrumentDescriptor, scope HostScope, value SampleValue, sets []LabelSet) {
137 mustFiniteSample(value)
138
139 c.mu.Lock()
140 defer c.mu.Unlock()
141
142 if c.active == nil {
143 panic(errCycleInactive)
144 }
145
146 schema := desc.histogram
147 if schema == nil || len(schema.bounds) == 0 {
148 panic(errHistogramBounds)
149 }
150
151 labels, labelsKey, err := labelsFromSet(sets, c)
152 if err != nil {
153 panic(err)
154 }
155 if labelsContainKey(labels, HistogramBucketLabel) {
156 panic(errHistogramLabelKey)
157 }
158 scope, ok := c.prepareHostScopeForWriteLocked(scope)
159 if !ok {
160 return
161 }
162
163 key := makeSeriesKey(scope.ScopeKey, desc.name, labelsKey)
164 entry, ok := c.active.histograms[key]
165 if !ok {
166 entry = &stagedHistogram{
167 key: key,
168 name: desc.name,
169 hostScopeKey: scope.ScopeKey,
170 hostScope: scope,
171 labels: labels,
172 labelsKey: labelsKey,
173 desc: desc,
174 bounds: append([]float64(nil), schema.bounds...),
175 cumulative: make([]SampleValue, len(schema.bounds)),
176 }
177 if desc.window == WindowCumulative {
178 if existing := c.snapshot.Load().series[key]; existing != nil && existing.desc != nil && existing.desc.kind == kindHistogram {
179 entry.count = existing.histogramCount
180 entry.sum = existing.histogramSum
181 entry.cumulative = append(entry.cumulative[:0], existing.histogramCumulative...)
182 if len(entry.cumulative) < len(schema.bounds) {
183 entry.cumulative = append(entry.cumulative, make([]SampleValue, len(schema.bounds)-len(entry.cumulative))...)
184 }
185 }
186 }
187 c.active.histograms[key] = entry
188 }
189
190 idx := findHistogramBucket(schema.bounds, value)
191 if idx < len(entry.cumulative) {
192 for i := idx; i < len(entry.cumulative); i++ {
193 entry.cumulative[i]++
194 }
195 }
196 entry.count++
197 entry.sum += value
198 }
199
200 func normalizeHistogramPoint(point HistogramPoint, schema *histogramSchema) ([]float64, SampleValue, SampleValue, []SampleValue) {
201 mustFiniteSample(point.Count)
202 mustFiniteSample(point.Sum)
203
204 if point.Count < 0 {
205 panic(fmt.Errorf("%w: negative count", errHistogramPoint))
206 }
207
208 bounds := make([]float64, 0, len(point.Buckets))
209 cumulative := make([]SampleValue, 0, len(point.Buckets))
210 prevBound := math.Inf(-1)
211 prevCount := SampleValue(0)
212 for i, b := range point.Buckets {
213 ub := b.UpperBound
214 if math.IsNaN(ub) || math.IsInf(ub, -1) {
215 panic(fmt.Errorf("%w: invalid upper bound", errHistogramPoint))
216 }
217 if math.IsInf(ub, +1) {
218 if i != len(point.Buckets)-1 {
219 panic(fmt.Errorf("%w: +Inf bucket must be last", errHistogramPoint))
220 }
221 // +Inf bucket is implicit.
222 continue
223 }
224 if ub <= prevBound {
225 panic(fmt.Errorf("%w: bounds must be strictly increasing", errHistogramPoint))
226 }
227 if b.CumulativeCount < prevCount {
228 panic(fmt.Errorf("%w: cumulative bucket counts must be monotonic", errHistogramPoint))
229 }
230 if b.CumulativeCount < 0 {
231 panic(fmt.Errorf("%w: cumulative bucket counts must be non-negative", errHistogramPoint))
232 }
233 mustFiniteSample(b.CumulativeCount)
234 bounds = append(bounds, ub)
235 cumulative = append(cumulative, b.CumulativeCount)
236 prevBound = ub
237 prevCount = b.CumulativeCount
238 }
239 if len(cumulative) > 0 && cumulative[len(cumulative)-1] > point.Count {
240 panic(fmt.Errorf("%w: last cumulative bucket exceeds count", errHistogramPoint))
241 }
242
243 if schema != nil {
244 if !equalHistogramBounds(schema.bounds, bounds) {
245 panic(fmt.Errorf("%w: bucket bounds mismatch", errHistogramPoint))
246 }
247 bounds = append([]float64(nil), schema.bounds...)
248 }
249
250 return bounds, point.Count, point.Sum, cumulative
251 }
252
253 // findHistogramBucket returns the index of the bucket for value, or len(bounds) for +Inf.
254 func findHistogramBucket(bounds []float64, value float64) int {
255 n := len(bounds)
256 if n == 0 {
257 return 0
258 }
259 if value <= bounds[0] {
260 return 0
261 }
262 if value > bounds[n-1] {
263 return n
264 }
265 if n < 35 {
266 for i, b := range bounds {
267 if value <= b {
268 return i
269 }
270 }
271 return n
272 }
273 return sort.SearchFloat64s(bounds, value)
274 }
275
276 func formatHistogramBucketLabel(v float64) string {
277 if math.IsInf(v, +1) {
278 return "+Inf"
279 }
280 return strconv.FormatFloat(v, 'g', -1, 64)
281 }