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