Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/util/testappender/internal/dtobuilder/dtobuilder.go
4099 views
1
package dtobuilder
2
3
import (
4
"math"
5
"sort"
6
"strconv"
7
"time"
8
9
dto "github.com/prometheus/client_model/go"
10
"github.com/prometheus/common/model"
11
"github.com/prometheus/prometheus/model/exemplar"
12
"github.com/prometheus/prometheus/model/labels"
13
"github.com/prometheus/prometheus/model/metadata"
14
"github.com/prometheus/prometheus/model/textparse"
15
"google.golang.org/protobuf/types/known/timestamppb"
16
"k8s.io/utils/pointer"
17
)
18
19
// Sample represents an individually written sample to a storage.Appender.
20
type Sample struct {
21
Labels labels.Labels
22
Timestamp int64
23
Value float64
24
PrintTimestamp bool
25
}
26
27
// SeriesExemplar represents an individually written exemplar to a
28
// storage.Appender.
29
type SeriesExemplar struct {
30
// Labels is the labels of the series exposing the exemplar, not the labels
31
// on the exemplar itself.
32
Labels labels.Labels
33
Exemplar exemplar.Exemplar
34
}
35
36
// Build converts a series of written samples, exemplars, and metadata into a
37
// slice of *dto.MetricFamily.
38
func Build(
39
samples map[string]Sample,
40
exemplars map[string]SeriesExemplar,
41
metadata map[string]metadata.Metadata,
42
) []*dto.MetricFamily {
43
44
b := builder{
45
Samples: samples,
46
Exemplars: exemplars,
47
Metadata: metadata,
48
49
familyLookup: make(map[string]*dto.MetricFamily),
50
}
51
return b.Build()
52
}
53
54
type builder struct {
55
Samples map[string]Sample
56
Exemplars map[string]SeriesExemplar
57
Metadata map[string]metadata.Metadata
58
59
families []*dto.MetricFamily
60
familyLookup map[string]*dto.MetricFamily
61
}
62
63
// Build converts the dtoBuilder's Samples, Exemplars, and Metadata into a set
64
// of []*dto.MetricFamily.
65
func (b *builder) Build() []*dto.MetricFamily {
66
// *dto.MetricFamily represents a set of samples for a given family of
67
// metrics. All metrics with the same __name__ belong to the same family.
68
//
69
// Each *dto.MetricFamily has a set of *dto.Metric, which contain individual
70
// samples within that family. The *dto.Metric is where non-__name__ labels
71
// are kept.
72
//
73
// *dto.Metrics can represent counters, gauges, summaries, histograms, and
74
// untyped values.
75
//
76
// In the case of a summary, the *dto.Metric contains multiple samples,
77
// holding each quantile, the _count, and the _sum. Similarly for histograms,
78
// the *dto.Metric contains each bucket, the _count, and the _sum.
79
//
80
// Because *dto.Metrics for summaries and histograms contain multiple
81
// samples, Build must roll up individually recorded samples into the
82
// appropriate *dto.Metric. See buildMetricsFromSamples for more information.
83
84
// We *must* do things in the following order:
85
//
86
// 1. Populate the families from metadata so we know what fields in
87
// *dto.Metric to set.
88
// 2. Populate *dto.Metric values from provided samples.
89
// 3. Assign exemplars to *dto.Metrics as appropriate.
90
b.buildFamiliesFromMetadata()
91
b.buildMetricsFromSamples()
92
b.injectExemplars()
93
94
// Sort all the data before returning.
95
sortMetricFamilies(b.families)
96
return b.families
97
}
98
99
// buildFamiliesFromMetadata populates the list of families based on the
100
// metadata known to the dtoBuilder. familyLookup will be updated for all
101
// metrics which map to the same family.
102
//
103
// In the case of summaries and histograms, multiple metrics map to the same
104
// family (the bucket/quantile, the _sum, and the _count metrics).
105
func (b *builder) buildFamiliesFromMetadata() {
106
for familyName, m := range b.Metadata {
107
mt := textParseToMetricType(m.Type)
108
mf := &dto.MetricFamily{
109
Name: pointer.String(familyName),
110
Type: &mt,
111
}
112
if m.Help != "" {
113
mf.Help = pointer.String(m.Help)
114
}
115
116
b.families = append(b.families, mf)
117
118
// Determine how to populate the lookup table.
119
switch mt {
120
case dto.MetricType_SUMMARY:
121
// Summaries include metrics with the family name (for quantiles),
122
// followed by _sum and _count suffixes.
123
b.familyLookup[familyName] = mf
124
b.familyLookup[familyName+"_sum"] = mf
125
b.familyLookup[familyName+"_count"] = mf
126
case dto.MetricType_HISTOGRAM:
127
// Histograms include metrics for _bucket, _sum, and _count suffixes.
128
b.familyLookup[familyName+"_bucket"] = mf
129
b.familyLookup[familyName+"_sum"] = mf
130
b.familyLookup[familyName+"_count"] = mf
131
default:
132
// Everything else matches the family name exactly.
133
b.familyLookup[familyName] = mf
134
}
135
}
136
}
137
138
func textParseToMetricType(tp textparse.MetricType) dto.MetricType {
139
switch tp {
140
case textparse.MetricTypeCounter:
141
return dto.MetricType_COUNTER
142
case textparse.MetricTypeGauge:
143
return dto.MetricType_GAUGE
144
case textparse.MetricTypeHistogram:
145
return dto.MetricType_HISTOGRAM
146
case textparse.MetricTypeSummary:
147
return dto.MetricType_SUMMARY
148
default:
149
// There are other values for m.Type, but they're all
150
// OpenMetrics-specific and we're only converting into the Prometheus
151
// exposition format.
152
return dto.MetricType_UNTYPED
153
}
154
}
155
156
// buildMetricsFromSamples populates *dto.Metrics. If the MetricFamily doesn't
157
// exist for a given sample, a new one is created.
158
func (b *builder) buildMetricsFromSamples() {
159
for _, sample := range b.Samples {
160
// Get or create the metric family.
161
metricName := sample.Labels.Get(model.MetricNameLabel)
162
mf := b.getOrCreateMetricFamily(metricName)
163
164
// Retrieve the *dto.Metric based on labels.
165
m := getOrCreateMetric(mf, sample.Labels)
166
if sample.PrintTimestamp {
167
m.TimestampMs = pointer.Int64(sample.Timestamp)
168
}
169
170
switch familyType(mf) {
171
case dto.MetricType_COUNTER:
172
m.Counter = &dto.Counter{
173
Value: pointer.Float64(sample.Value),
174
}
175
176
case dto.MetricType_GAUGE:
177
m.Gauge = &dto.Gauge{
178
Value: pointer.Float64(sample.Value),
179
}
180
181
case dto.MetricType_SUMMARY:
182
if m.Summary == nil {
183
m.Summary = &dto.Summary{}
184
}
185
186
switch {
187
case metricName == mf.GetName()+"_count":
188
val := uint64(sample.Value)
189
m.Summary.SampleCount = &val
190
case metricName == mf.GetName()+"_sum":
191
m.Summary.SampleSum = pointer.Float64(sample.Value)
192
case metricName == mf.GetName():
193
quantile, err := strconv.ParseFloat(sample.Labels.Get(model.QuantileLabel), 64)
194
if err != nil {
195
continue
196
}
197
198
m.Summary.Quantile = append(m.Summary.Quantile, &dto.Quantile{
199
Quantile: &quantile,
200
Value: pointer.Float64(sample.Value),
201
})
202
}
203
204
case dto.MetricType_UNTYPED:
205
m.Untyped = &dto.Untyped{
206
Value: pointer.Float64(sample.Value),
207
}
208
209
case dto.MetricType_HISTOGRAM:
210
if m.Histogram == nil {
211
m.Histogram = &dto.Histogram{}
212
}
213
214
switch {
215
case metricName == mf.GetName()+"_count":
216
val := uint64(sample.Value)
217
m.Histogram.SampleCount = &val
218
case metricName == mf.GetName()+"_sum":
219
m.Histogram.SampleSum = pointer.Float64(sample.Value)
220
case metricName == mf.GetName()+"_bucket":
221
boundary, err := strconv.ParseFloat(sample.Labels.Get(model.BucketLabel), 64)
222
if err != nil {
223
continue
224
}
225
226
count := uint64(sample.Value)
227
228
m.Histogram.Bucket = append(m.Histogram.Bucket, &dto.Bucket{
229
UpperBound: &boundary,
230
CumulativeCount: &count,
231
})
232
}
233
}
234
}
235
}
236
237
func (b *builder) getOrCreateMetricFamily(familyName string) *dto.MetricFamily {
238
mf, ok := b.familyLookup[familyName]
239
if ok {
240
return mf
241
}
242
243
mt := dto.MetricType_UNTYPED
244
mf = &dto.MetricFamily{
245
Name: &familyName,
246
Type: &mt,
247
}
248
b.families = append(b.families, mf)
249
b.familyLookup[familyName] = mf
250
return mf
251
}
252
253
func getOrCreateMetric(mf *dto.MetricFamily, l labels.Labels) *dto.Metric {
254
metricLabels := toLabelPairs(familyType(mf), l)
255
256
for _, check := range mf.Metric {
257
if labelPairsEqual(check.Label, metricLabels) {
258
return check
259
}
260
}
261
262
m := &dto.Metric{
263
Label: metricLabels,
264
}
265
mf.Metric = append(mf.Metric, m)
266
return m
267
}
268
269
// toLabelPairs converts labels.Labels into []*dto.LabelPair. The __name__
270
// label is always dropped, since the metric name is retrieved from the family
271
// name instead.
272
//
273
// The quantile label is dropped for summaries, and the le label is dropped for
274
// histograms.
275
func toLabelPairs(mt dto.MetricType, ls labels.Labels) []*dto.LabelPair {
276
res := make([]*dto.LabelPair, 0, len(ls))
277
for _, l := range ls {
278
if l.Name == model.MetricNameLabel {
279
continue
280
} else if l.Name == model.QuantileLabel && mt == dto.MetricType_SUMMARY {
281
continue
282
} else if l.Name == model.BucketLabel && mt == dto.MetricType_HISTOGRAM {
283
continue
284
}
285
286
res = append(res, &dto.LabelPair{
287
Name: pointer.String(l.Name),
288
Value: pointer.String(l.Value),
289
})
290
}
291
292
sort.Slice(res, func(i, j int) bool {
293
switch {
294
case *res[i].Name < *res[j].Name:
295
return true
296
case *res[i].Value < *res[j].Value:
297
return true
298
default:
299
return false
300
}
301
})
302
return res
303
}
304
305
func labelPairsEqual(a, b []*dto.LabelPair) bool {
306
if len(a) != len(b) {
307
return false
308
}
309
310
for i := 0; i < len(a); i++ {
311
if *a[i].Name != *b[i].Name || *a[i].Value != *b[i].Value {
312
return false
313
}
314
}
315
316
return true
317
}
318
319
func familyType(mf *dto.MetricFamily) dto.MetricType {
320
ty := mf.Type
321
if ty == nil {
322
return dto.MetricType_UNTYPED
323
}
324
return *ty
325
}
326
327
// injectExemplars populates the exemplars in the various *dto.Metric
328
// instances. Exemplars are ignored if the parent *dto.MetricFamily doesn't
329
// support exeplars based on metric type.
330
func (b *builder) injectExemplars() {
331
for _, e := range b.Exemplars {
332
// Get or create the metric family.
333
exemplarName := e.Labels.Get(model.MetricNameLabel)
334
335
mf, ok := b.familyLookup[exemplarName]
336
if !ok {
337
// No metric family, which means no corresponding sample; ignore.
338
continue
339
}
340
341
m := getMetric(mf, e.Labels)
342
if m == nil {
343
continue
344
}
345
346
// Only counters and histograms support exemplars.
347
switch familyType(mf) {
348
case dto.MetricType_COUNTER:
349
if m.Counter == nil {
350
// Sample never added; ignore.
351
continue
352
}
353
m.Counter.Exemplar = convertExemplar(dto.MetricType_COUNTER, e.Exemplar)
354
case dto.MetricType_HISTOGRAM:
355
if m.Histogram == nil {
356
// Sample never added; ignore.
357
continue
358
}
359
360
switch {
361
case exemplarName == mf.GetName()+"_bucket":
362
boundary, err := strconv.ParseFloat(e.Labels.Get(model.BucketLabel), 64)
363
if err != nil {
364
continue
365
}
366
bucket := findBucket(m.Histogram, boundary)
367
if bucket == nil {
368
continue
369
}
370
bucket.Exemplar = convertExemplar(dto.MetricType_HISTOGRAM, e.Exemplar)
371
}
372
}
373
}
374
}
375
376
func getMetric(mf *dto.MetricFamily, l labels.Labels) *dto.Metric {
377
metricLabels := toLabelPairs(familyType(mf), l)
378
379
for _, check := range mf.Metric {
380
if labelPairsEqual(check.Label, metricLabels) {
381
return check
382
}
383
}
384
385
return nil
386
}
387
388
func convertExemplar(mt dto.MetricType, e exemplar.Exemplar) *dto.Exemplar {
389
res := &dto.Exemplar{
390
Label: toLabelPairs(mt, e.Labels),
391
Value: &e.Value,
392
}
393
if e.HasTs {
394
res.Timestamp = timestamppb.New(time.UnixMilli(e.Ts))
395
}
396
return res
397
}
398
399
func findBucket(h *dto.Histogram, bound float64) *dto.Bucket {
400
for _, b := range h.GetBucket() {
401
// If it's close enough, use the bucket.
402
if math.Abs(b.GetUpperBound()-bound) < 1e-9 {
403
return b
404
}
405
}
406
407
return nil
408
}
409
410