master
go 406 lines 11.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package prometheus
4
5 import (
6 "errors"
7 "fmt"
8 "io"
9 "strings"
10
11 "github.com/prometheus/common/model"
12 "github.com/prometheus/prometheus/model/labels"
13 "github.com/prometheus/prometheus/model/textparse"
14
15 "github.com/netdata/netdata/go/plugins/pkg/prometheus/selector"
16 )
17
18 const (
19 quantileLabel = "quantile"
20 bucketLabel = "le"
21 )
22
23 const (
24 countSuffix = "_count"
25 sumSuffix = "_sum"
26 bucketSuffix = "_bucket"
27 )
28
29 // promTextParser orchestrates a single parse pass. The driver parses the
30 // exposition once into a flat sample stream; the assembler folds that stream
31 // into typed MetricFamilies. Scrape() (families), ScrapeSeries() (Series), and
32 // the exported sample stream are all produced from this one model.
33 type promTextParser struct {
34 sr selector.Selector
35
36 driver parseDriver
37 asm assembler
38 series Series
39 }
40
41 func (p *promTextParser) parseToMetricFamilies(text []byte, transform SampleTransform) (MetricFamilies, error) {
42 p.driver.sr = p.sr
43 p.asm.reset()
44
45 // With no transform, ownLabels=false: the assembler copies labels into its own
46 // buffers, so the driver may lend the scratch label set (the no-allocation fast
47 // path). A transform may mutate or retain a sample's labels, so it needs the
48 // sample to own them (ownLabels=true).
49 onSample := p.asm.applySample
50 ownLabels := false
51 if transform != nil {
52 ownLabels = true
53 onSample = func(s Sample) error {
54 s, keep, err := transform(s)
55 if err != nil {
56 return err
57 }
58 if !keep {
59 return nil
60 }
61 return p.asm.applySample(s)
62 }
63 }
64
65 if err := p.driver.parseSamples(text, ownLabels, p.asm.applyHelp, onSample); err != nil {
66 return nil, err
67 }
68
69 return p.asm.families(), nil
70 }
71
72 func (p *promTextParser) parseToSeries(text []byte) (Series, error) {
73 p.driver.sr = p.sr
74 p.series.Reset()
75
76 // Series keeps the raw label set straight from textparse (sorted, with __name__
77 // in its sorted position) — identical to the legacy parser. It does NOT go
78 // through the Sample model (which separates __name__), so there is no deferral,
79 // no reordering, and the sorted-label invariant is preserved.
80 err := p.driver.iterate(text, nil, nil, func(series labels.Labels, value float64) error {
81 p.series.Add(SeriesSample{Labels: copyLabels(series), Value: value})
82 return nil
83 })
84 if err != nil {
85 return nil, err
86 }
87
88 p.series.Sort()
89
90 return p.series, nil
91 }
92
93 // parseDriver runs the single exposition parse pass (iterate). On top of it,
94 // parseSamples emits a flat, classified sample stream, deferring a _sum/_count
95 // whose family type is not yet known and back-resolving it once the type appears
96 // (a later # TYPE, _bucket, or quantile series) or at EOF. familyTypes/pending
97 // hold that deferral state.
98 type parseDriver struct {
99 sr selector.Selector
100
101 familyTypes map[string]model.MetricType
102 pending []pendingSample
103 currSeries labels.Labels
104 }
105
106 type pendingSample struct {
107 baseName string
108 sample Sample
109 role pendingRole
110 }
111
112 type pendingRole uint8
113
114 const (
115 pendingNone pendingRole = iota
116 pendingSum
117 pendingCount
118 )
119
120 // iterate runs the shared single-pass exposition loop. For every series it calls
121 // onSeries with the raw label set (textparse order — sorted, __name__ in its
122 // sorted position) and value, after applying the selector; onHelp/onType deliver
123 // per-family metadata. This is the one parse loop: ScrapeSeries consumes it
124 // directly (raw labels, byte-identical to the legacy parser), while the flat
125 // sample stream is layered on top by parseSamples.
126 func (d *parseDriver) iterate(
127 text []byte,
128 onHelp func(name, help string),
129 onType func(name string, typ model.MetricType) error,
130 onSeries func(series labels.Labels, value float64) error,
131 ) error {
132 parser := textparse.NewPromParser(text, labels.NewSymbolTable())
133 for {
134 entry, err := parser.Next()
135 if err != nil {
136 if errors.Is(err, io.EOF) {
137 break
138 }
139 if entry == textparse.EntryInvalid && strings.HasPrefix(err.Error(), "invalid metric type") {
140 continue
141 }
142 return fmt.Errorf("failed to parse prometheus metrics: %v", err)
143 }
144
145 switch entry {
146 case textparse.EntryHelp:
147 if onHelp != nil {
148 name, help := parser.Help()
149 onHelp(string(name), sanitizeHelp(string(help)))
150 }
151 case textparse.EntryType:
152 if onType != nil {
153 name, typ := parser.Type()
154 if err := onType(string(name), typ); err != nil {
155 return err
156 }
157 }
158 case textparse.EntrySeries:
159 d.currSeries = d.currSeries[:0]
160 parser.Metric(&d.currSeries)
161
162 if d.sr != nil && !d.sr.Matches(d.currSeries) {
163 continue
164 }
165
166 _, _, value := parser.Series()
167
168 if onSeries != nil {
169 if err := onSeries(d.currSeries, value); err != nil {
170 return err
171 }
172 }
173 }
174 }
175
176 return nil
177 }
178
179 // parseSamples layers the flat, classified sample stream on top of iterate. It
180 // turns each series into a Sample (Kind + FamilyType) and defers a _sum/_count
181 // whose family type is not yet known, back-resolving it once the type appears (a
182 // later # TYPE, _bucket, or quantile series) or flushing it at EOF. Deferral can
183 // emit a _sum/_count after a later, unrelated series — see ScrapeWithTransform's doc.
184 func (d *parseDriver) parseSamples(text []byte, ownLabels bool, onHelp func(name, help string), onSample func(Sample) error) error {
185 d.reset()
186
187 err := d.iterate(text, onHelp,
188 func(name string, typ model.MetricType) error {
189 d.familyTypes[name] = typ
190 var err error
191 d.pending, err = emitResolvedPending(d.pending, name, typ, onSample)
192 return err
193 },
194 func(series labels.Labels, value float64) error {
195 sample, baseName, role, ok := d.makeSample(series, value, ownLabels)
196 if !ok {
197 return nil
198 }
199
200 // A quantile/bucket series reveals the family type; back-resolve any
201 // _sum/_count buffered before it.
202 switch sample.Kind {
203 case SampleKindSummaryQuantile:
204 var err error
205 d.pending, err = emitResolvedPending(d.pending, sample.Name, model.MetricTypeSummary, onSample)
206 if err != nil {
207 return err
208 }
209 case SampleKindHistogramBucket:
210 var err error
211 d.pending, err = emitResolvedPending(d.pending, strings.TrimSuffix(sample.Name, bucketSuffix), model.MetricTypeHistogram, onSample)
212 if err != nil {
213 return err
214 }
215 }
216
217 if role != pendingNone {
218 if !ownLabels {
219 sample.Labels = copyLabels(sample.Labels)
220 }
221 d.pending = append(d.pending, pendingSample{
222 baseName: baseName,
223 sample: sample,
224 role: role,
225 })
226 return nil
227 }
228
229 return onSample(sample)
230 },
231 )
232 if err != nil {
233 return err
234 }
235
236 // Flush still-unresolved _sum/_count as plain scalars (matches the legacy
237 // behavior for a _sum/_count whose family type never appears).
238 for _, ps := range d.pending {
239 if err := onSample(ps.sample); err != nil {
240 return err
241 }
242 }
243 d.pending = d.pending[:0]
244
245 return nil
246 }
247
248 func (d *parseDriver) makeSample(series labels.Labels, value float64, ownLabels bool) (Sample, string, pendingRole, bool) {
249 name, ok := metricNameValue(series)
250 if !ok {
251 return Sample{}, "", pendingNone, false
252 }
253
254 var lbs labels.Labels
255 if ownLabels {
256 lbs = copyLabelsWithoutName(series)
257 } else {
258 lbs, _, _ = removeLabel(series, labels.MetricName)
259 }
260
261 sample := Sample{
262 Name: name,
263 Labels: lbs,
264 Value: value,
265 Kind: SampleKindScalar,
266 FamilyType: d.familyTypes[name],
267 }
268 if sample.FamilyType == "" {
269 sample.FamilyType = model.MetricTypeUnknown
270 }
271
272 if sample.Labels.Has(quantileLabel) {
273 if sample.FamilyType != model.MetricTypeUnknown && sample.FamilyType != model.MetricTypeSummary {
274 return sample, "", pendingNone, true
275 }
276 sample.Kind = SampleKindSummaryQuantile
277 sample.FamilyType = model.MetricTypeSummary
278 d.familyTypes[name] = model.MetricTypeSummary
279 return sample, "", pendingNone, true
280 }
281
282 // A histogram bucket requires an "le" label. A _bucket-named series without le
283 // is malformed: it is NOT treated as a bucket but falls through to a plain
284 // metric, preserving its value. (The legacy parser folded such a series into
285 // the histogram family and dropped its value; valid buckets always carry le,
286 // so real input is unaffected.)
287 if strings.HasSuffix(name, bucketSuffix) && sample.Labels.Has(bucketLabel) {
288 if sample.FamilyType != model.MetricTypeUnknown && sample.FamilyType != model.MetricTypeHistogram {
289 return sample, "", pendingNone, true
290 }
291 baseName := strings.TrimSuffix(name, bucketSuffix)
292 sample.Kind = SampleKindHistogramBucket
293 sample.FamilyType = model.MetricTypeHistogram
294 d.familyTypes[baseName] = model.MetricTypeHistogram
295 return sample, "", pendingNone, true
296 }
297
298 if strings.HasSuffix(name, sumSuffix) {
299 if sample.FamilyType != model.MetricTypeUnknown &&
300 sample.FamilyType != model.MetricTypeSummary &&
301 sample.FamilyType != model.MetricTypeHistogram {
302 return sample, "", pendingNone, true
303 }
304
305 baseName := strings.TrimSuffix(name, sumSuffix)
306 switch d.familyTypes[baseName] {
307 case model.MetricTypeSummary:
308 sample.Kind = SampleKindSummarySum
309 sample.FamilyType = model.MetricTypeSummary
310 return sample, "", pendingNone, true
311 case model.MetricTypeHistogram:
312 sample.Kind = SampleKindHistogramSum
313 sample.FamilyType = model.MetricTypeHistogram
314 return sample, "", pendingNone, true
315 default:
316 return sample, baseName, pendingSum, true
317 }
318 }
319
320 if strings.HasSuffix(name, countSuffix) {
321 if sample.FamilyType != model.MetricTypeUnknown &&
322 sample.FamilyType != model.MetricTypeSummary &&
323 sample.FamilyType != model.MetricTypeHistogram {
324 return sample, "", pendingNone, true
325 }
326
327 baseName := strings.TrimSuffix(name, countSuffix)
328 switch d.familyTypes[baseName] {
329 case model.MetricTypeSummary:
330 sample.Kind = SampleKindSummaryCount
331 sample.FamilyType = model.MetricTypeSummary
332 return sample, "", pendingNone, true
333 case model.MetricTypeHistogram:
334 sample.Kind = SampleKindHistogramCount
335 sample.FamilyType = model.MetricTypeHistogram
336 return sample, "", pendingNone, true
337 default:
338 return sample, baseName, pendingCount, true
339 }
340 }
341
342 return sample, "", pendingNone, true
343 }
344
345 func (d *parseDriver) reset() {
346 d.currSeries = d.currSeries[:0]
347
348 if d.familyTypes == nil {
349 d.familyTypes = make(map[string]model.MetricType)
350 }
351 for k := range d.familyTypes {
352 delete(d.familyTypes, k)
353 }
354
355 d.pending = d.pending[:0]
356 }
357
358 // emitResolvedPending flushes buffered _sum/_count samples for baseName now that
359 // its family type is known, emitting them in buffered (exposition) order.
360 func emitResolvedPending(pending []pendingSample, baseName string, typ model.MetricType, onSample func(Sample) error) ([]pendingSample, error) {
361 if len(pending) == 0 {
362 return pending, nil
363 }
364
365 out := pending[:0]
366 for _, ps := range pending {
367 if ps.baseName != baseName {
368 out = append(out, ps)
369 continue
370 }
371
372 sample := ps.sample
373 sample.FamilyType = typ
374 switch typ {
375 case model.MetricTypeSummary:
376 if ps.role == pendingSum {
377 sample.Kind = SampleKindSummarySum
378 } else {
379 sample.Kind = SampleKindSummaryCount
380 }
381 case model.MetricTypeHistogram:
382 if ps.role == pendingSum {
383 sample.Kind = SampleKindHistogramSum
384 } else {
385 sample.Kind = SampleKindHistogramCount
386 }
387 default:
388 sample.Kind = SampleKindScalar
389 sample.FamilyType = model.MetricTypeUnknown
390 }
391
392 if err := onSample(sample); err != nil {
393 return nil, err
394 }
395 }
396
397 return out, nil
398 }
399
400 func sanitizeHelp(help string) string {
401 if strings.IndexByte(help, '\n') == -1 {
402 return help
403 }
404 // HELP is used as a chart title; collapse multiline help to one line.
405 return strings.Join(strings.Fields(help), " ")
406 }