blob: c4e9c1bbc3a79c833bf13143cc3b15e740e448db [file] [log] [blame]
khenaidooab1f7bd2019-11-14 14:00:27 -05001// Copyright 2014 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package expfmt
15
16import (
khenaidood948f772021-08-11 17:49:24 -040017 "bufio"
khenaidooab1f7bd2019-11-14 14:00:27 -050018 "fmt"
19 "io"
20 "math"
21 "strconv"
22 "strings"
23 "sync"
24
khenaidooab1f7bd2019-11-14 14:00:27 -050025 dto "github.com/prometheus/client_model/go"
Abhay Kumara2ae5992025-11-10 14:02:24 +000026
27 "github.com/prometheus/common/model"
khenaidooab1f7bd2019-11-14 14:00:27 -050028)
29
khenaidood948f772021-08-11 17:49:24 -040030// enhancedWriter has all the enhanced write functions needed here. bufio.Writer
khenaidooab1f7bd2019-11-14 14:00:27 -050031// implements it.
32type enhancedWriter interface {
33 io.Writer
34 WriteRune(r rune) (n int, err error)
35 WriteString(s string) (n int, err error)
36 WriteByte(c byte) error
37}
38
39const (
khenaidooab1f7bd2019-11-14 14:00:27 -050040 initialNumBufSize = 24
41)
42
43var (
44 bufPool = sync.Pool{
45 New: func() interface{} {
Abhay Kumara2ae5992025-11-10 14:02:24 +000046 return bufio.NewWriter(io.Discard)
khenaidooab1f7bd2019-11-14 14:00:27 -050047 },
48 }
49 numBufPool = sync.Pool{
50 New: func() interface{} {
51 b := make([]byte, 0, initialNumBufSize)
52 return &b
53 },
54 }
55)
56
57// MetricFamilyToText converts a MetricFamily proto message into text format and
58// writes the resulting lines to 'out'. It returns the number of bytes written
59// and any error encountered. The output will have the same order as the input,
60// no further sorting is performed. Furthermore, this function assumes the input
61// is already sanitized and does not perform any sanity checks. If the input
62// contains duplicate metrics or invalid metric or label names, the conversion
63// will result in invalid text format output.
64//
Abhay Kumara2ae5992025-11-10 14:02:24 +000065// If metric names conform to the legacy validation pattern, they will be placed
66// outside the brackets in the traditional way, like `foo{}`. If the metric name
67// fails the legacy validation check, it will be placed quoted inside the
68// brackets: `{"foo"}`. As stated above, the input is assumed to be santized and
69// no error will be thrown in this case.
70//
71// Similar to metric names, if label names conform to the legacy validation
72// pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label
73// name fails the legacy validation check, it will be quoted:
74// `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and
75// no error will be thrown in this case.
76//
khenaidooab1f7bd2019-11-14 14:00:27 -050077// This method fulfills the type 'prometheus.encoder'.
78func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err error) {
79 // Fail-fast checks.
80 if len(in.Metric) == 0 {
81 return 0, fmt.Errorf("MetricFamily has no metrics: %s", in)
82 }
83 name := in.GetName()
84 if name == "" {
85 return 0, fmt.Errorf("MetricFamily has no name: %s", in)
86 }
87
88 // Try the interface upgrade. If it doesn't work, we'll use a
khenaidood948f772021-08-11 17:49:24 -040089 // bufio.Writer from the sync.Pool.
khenaidooab1f7bd2019-11-14 14:00:27 -050090 w, ok := out.(enhancedWriter)
91 if !ok {
khenaidood948f772021-08-11 17:49:24 -040092 b := bufPool.Get().(*bufio.Writer)
93 b.Reset(out)
khenaidooab1f7bd2019-11-14 14:00:27 -050094 w = b
95 defer func() {
khenaidood948f772021-08-11 17:49:24 -040096 bErr := b.Flush()
khenaidooab1f7bd2019-11-14 14:00:27 -050097 if err == nil {
98 err = bErr
99 }
100 bufPool.Put(b)
101 }()
102 }
103
104 var n int
105
106 // Comments, first HELP, then TYPE.
107 if in.Help != nil {
108 n, err = w.WriteString("# HELP ")
109 written += n
110 if err != nil {
111 return
112 }
Abhay Kumara2ae5992025-11-10 14:02:24 +0000113 n, err = writeName(w, name)
khenaidooab1f7bd2019-11-14 14:00:27 -0500114 written += n
115 if err != nil {
116 return
117 }
118 err = w.WriteByte(' ')
119 written++
120 if err != nil {
121 return
122 }
123 n, err = writeEscapedString(w, *in.Help, false)
124 written += n
125 if err != nil {
126 return
127 }
128 err = w.WriteByte('\n')
129 written++
130 if err != nil {
131 return
132 }
133 }
134 n, err = w.WriteString("# TYPE ")
135 written += n
136 if err != nil {
137 return
138 }
Abhay Kumara2ae5992025-11-10 14:02:24 +0000139 n, err = writeName(w, name)
khenaidooab1f7bd2019-11-14 14:00:27 -0500140 written += n
141 if err != nil {
142 return
143 }
144 metricType := in.GetType()
145 switch metricType {
146 case dto.MetricType_COUNTER:
147 n, err = w.WriteString(" counter\n")
148 case dto.MetricType_GAUGE:
149 n, err = w.WriteString(" gauge\n")
150 case dto.MetricType_SUMMARY:
151 n, err = w.WriteString(" summary\n")
152 case dto.MetricType_UNTYPED:
153 n, err = w.WriteString(" untyped\n")
154 case dto.MetricType_HISTOGRAM:
155 n, err = w.WriteString(" histogram\n")
156 default:
157 return written, fmt.Errorf("unknown metric type %s", metricType.String())
158 }
159 written += n
160 if err != nil {
161 return
162 }
163
164 // Finally the samples, one line for each.
165 for _, metric := range in.Metric {
166 switch metricType {
167 case dto.MetricType_COUNTER:
168 if metric.Counter == nil {
169 return written, fmt.Errorf(
170 "expected counter in metric %s %s", name, metric,
171 )
172 }
173 n, err = writeSample(
174 w, name, "", metric, "", 0,
175 metric.Counter.GetValue(),
176 )
177 case dto.MetricType_GAUGE:
178 if metric.Gauge == nil {
179 return written, fmt.Errorf(
180 "expected gauge in metric %s %s", name, metric,
181 )
182 }
183 n, err = writeSample(
184 w, name, "", metric, "", 0,
185 metric.Gauge.GetValue(),
186 )
187 case dto.MetricType_UNTYPED:
188 if metric.Untyped == nil {
189 return written, fmt.Errorf(
190 "expected untyped in metric %s %s", name, metric,
191 )
192 }
193 n, err = writeSample(
194 w, name, "", metric, "", 0,
195 metric.Untyped.GetValue(),
196 )
197 case dto.MetricType_SUMMARY:
198 if metric.Summary == nil {
199 return written, fmt.Errorf(
200 "expected summary in metric %s %s", name, metric,
201 )
202 }
203 for _, q := range metric.Summary.Quantile {
204 n, err = writeSample(
205 w, name, "", metric,
206 model.QuantileLabel, q.GetQuantile(),
207 q.GetValue(),
208 )
209 written += n
210 if err != nil {
211 return
212 }
213 }
214 n, err = writeSample(
215 w, name, "_sum", metric, "", 0,
216 metric.Summary.GetSampleSum(),
217 )
218 written += n
219 if err != nil {
220 return
221 }
222 n, err = writeSample(
223 w, name, "_count", metric, "", 0,
224 float64(metric.Summary.GetSampleCount()),
225 )
226 case dto.MetricType_HISTOGRAM:
227 if metric.Histogram == nil {
228 return written, fmt.Errorf(
229 "expected histogram in metric %s %s", name, metric,
230 )
231 }
232 infSeen := false
233 for _, b := range metric.Histogram.Bucket {
234 n, err = writeSample(
235 w, name, "_bucket", metric,
236 model.BucketLabel, b.GetUpperBound(),
237 float64(b.GetCumulativeCount()),
238 )
239 written += n
240 if err != nil {
241 return
242 }
243 if math.IsInf(b.GetUpperBound(), +1) {
244 infSeen = true
245 }
246 }
247 if !infSeen {
248 n, err = writeSample(
249 w, name, "_bucket", metric,
250 model.BucketLabel, math.Inf(+1),
251 float64(metric.Histogram.GetSampleCount()),
252 )
253 written += n
254 if err != nil {
255 return
256 }
257 }
258 n, err = writeSample(
259 w, name, "_sum", metric, "", 0,
260 metric.Histogram.GetSampleSum(),
261 )
262 written += n
263 if err != nil {
264 return
265 }
266 n, err = writeSample(
267 w, name, "_count", metric, "", 0,
268 float64(metric.Histogram.GetSampleCount()),
269 )
270 default:
271 return written, fmt.Errorf(
272 "unexpected type in metric %s %s", name, metric,
273 )
274 }
275 written += n
276 if err != nil {
277 return
278 }
279 }
280 return
281}
282
283// writeSample writes a single sample in text format to w, given the metric
284// name, the metric proto message itself, optionally an additional label name
285// with a float64 value (use empty string as label name if not required), and
286// the value. The function returns the number of bytes written and any error
287// encountered.
288func writeSample(
289 w enhancedWriter,
290 name, suffix string,
291 metric *dto.Metric,
292 additionalLabelName string, additionalLabelValue float64,
293 value float64,
294) (int, error) {
Abhay Kumara2ae5992025-11-10 14:02:24 +0000295 written := 0
296 n, err := writeNameAndLabelPairs(
297 w, name+suffix, metric.Label, additionalLabelName, additionalLabelValue,
khenaidooab1f7bd2019-11-14 14:00:27 -0500298 )
299 written += n
300 if err != nil {
301 return written, err
302 }
303 err = w.WriteByte(' ')
304 written++
305 if err != nil {
306 return written, err
307 }
308 n, err = writeFloat(w, value)
309 written += n
310 if err != nil {
311 return written, err
312 }
313 if metric.TimestampMs != nil {
314 err = w.WriteByte(' ')
315 written++
316 if err != nil {
317 return written, err
318 }
319 n, err = writeInt(w, *metric.TimestampMs)
320 written += n
321 if err != nil {
322 return written, err
323 }
324 }
325 err = w.WriteByte('\n')
326 written++
327 if err != nil {
328 return written, err
329 }
330 return written, nil
331}
332
Abhay Kumara2ae5992025-11-10 14:02:24 +0000333// writeNameAndLabelPairs converts a slice of LabelPair proto messages plus the
334// explicitly given metric name and additional label pair into text formatted as
335// required by the text format and writes it to 'w'. An empty slice in
336// combination with an empty string 'additionalLabelName' results in nothing
337// being written. Otherwise, the label pairs are written, escaped as required by
338// the text format, and enclosed in '{...}'. The function returns the number of
339// bytes written and any error encountered. If the metric name is not
340// legacy-valid, it will be put inside the brackets as well. Legacy-invalid
341// label names will also be quoted.
342func writeNameAndLabelPairs(
khenaidooab1f7bd2019-11-14 14:00:27 -0500343 w enhancedWriter,
Abhay Kumara2ae5992025-11-10 14:02:24 +0000344 name string,
khenaidooab1f7bd2019-11-14 14:00:27 -0500345 in []*dto.LabelPair,
346 additionalLabelName string, additionalLabelValue float64,
347) (int, error) {
khenaidooab1f7bd2019-11-14 14:00:27 -0500348 var (
Abhay Kumara2ae5992025-11-10 14:02:24 +0000349 written int
350 separator byte = '{'
351 metricInsideBraces = false
khenaidooab1f7bd2019-11-14 14:00:27 -0500352 )
Abhay Kumara2ae5992025-11-10 14:02:24 +0000353
354 if name != "" {
355 // If the name does not pass the legacy validity check, we must put the
356 // metric name inside the braces.
357 if !model.LegacyValidation.IsValidMetricName(name) {
358 metricInsideBraces = true
359 err := w.WriteByte(separator)
360 written++
361 if err != nil {
362 return written, err
363 }
364 separator = ','
365 }
366 n, err := writeName(w, name)
367 written += n
368 if err != nil {
369 return written, err
370 }
371 }
372
373 if len(in) == 0 && additionalLabelName == "" {
374 if metricInsideBraces {
375 err := w.WriteByte('}')
376 written++
377 if err != nil {
378 return written, err
379 }
380 }
381 return written, nil
382 }
383
khenaidooab1f7bd2019-11-14 14:00:27 -0500384 for _, lp := range in {
385 err := w.WriteByte(separator)
386 written++
387 if err != nil {
388 return written, err
389 }
Abhay Kumara2ae5992025-11-10 14:02:24 +0000390 n, err := writeName(w, lp.GetName())
khenaidooab1f7bd2019-11-14 14:00:27 -0500391 written += n
392 if err != nil {
393 return written, err
394 }
395 n, err = w.WriteString(`="`)
396 written += n
397 if err != nil {
398 return written, err
399 }
400 n, err = writeEscapedString(w, lp.GetValue(), true)
401 written += n
402 if err != nil {
403 return written, err
404 }
405 err = w.WriteByte('"')
406 written++
407 if err != nil {
408 return written, err
409 }
410 separator = ','
411 }
412 if additionalLabelName != "" {
413 err := w.WriteByte(separator)
414 written++
415 if err != nil {
416 return written, err
417 }
418 n, err := w.WriteString(additionalLabelName)
419 written += n
420 if err != nil {
421 return written, err
422 }
423 n, err = w.WriteString(`="`)
424 written += n
425 if err != nil {
426 return written, err
427 }
428 n, err = writeFloat(w, additionalLabelValue)
429 written += n
430 if err != nil {
431 return written, err
432 }
433 err = w.WriteByte('"')
434 written++
435 if err != nil {
436 return written, err
437 }
438 }
439 err := w.WriteByte('}')
440 written++
441 if err != nil {
442 return written, err
443 }
444 return written, nil
445}
446
447// writeEscapedString replaces '\' by '\\', new line character by '\n', and - if
448// includeDoubleQuote is true - '"' by '\"'.
449var (
450 escaper = strings.NewReplacer("\\", `\\`, "\n", `\n`)
451 quotedEscaper = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)
452)
453
454func writeEscapedString(w enhancedWriter, v string, includeDoubleQuote bool) (int, error) {
455 if includeDoubleQuote {
456 return quotedEscaper.WriteString(w, v)
khenaidooab1f7bd2019-11-14 14:00:27 -0500457 }
khenaidood948f772021-08-11 17:49:24 -0400458 return escaper.WriteString(w, v)
khenaidooab1f7bd2019-11-14 14:00:27 -0500459}
460
461// writeFloat is equivalent to fmt.Fprint with a float64 argument but hardcodes
462// a few common cases for increased efficiency. For non-hardcoded cases, it uses
463// strconv.AppendFloat to avoid allocations, similar to writeInt.
464func writeFloat(w enhancedWriter, f float64) (int, error) {
465 switch {
466 case f == 1:
467 return 1, w.WriteByte('1')
468 case f == 0:
469 return 1, w.WriteByte('0')
470 case f == -1:
471 return w.WriteString("-1")
472 case math.IsNaN(f):
473 return w.WriteString("NaN")
474 case math.IsInf(f, +1):
475 return w.WriteString("+Inf")
476 case math.IsInf(f, -1):
477 return w.WriteString("-Inf")
478 default:
479 bp := numBufPool.Get().(*[]byte)
480 *bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64)
481 written, err := w.Write(*bp)
482 numBufPool.Put(bp)
483 return written, err
484 }
485}
486
487// writeInt is equivalent to fmt.Fprint with an int64 argument but uses
488// strconv.AppendInt with a byte slice taken from a sync.Pool to avoid
489// allocations.
490func writeInt(w enhancedWriter, i int64) (int, error) {
491 bp := numBufPool.Get().(*[]byte)
492 *bp = strconv.AppendInt((*bp)[:0], i, 10)
493 written, err := w.Write(*bp)
494 numBufPool.Put(bp)
495 return written, err
496}
Abhay Kumara2ae5992025-11-10 14:02:24 +0000497
498// writeName writes a string as-is if it complies with the legacy naming
499// scheme, or escapes it in double quotes if not.
500func writeName(w enhancedWriter, name string) (int, error) {
501 if model.LegacyValidation.IsValidMetricName(name) {
502 return w.WriteString(name)
503 }
504 var written int
505 var err error
506 err = w.WriteByte('"')
507 written++
508 if err != nil {
509 return written, err
510 }
511 var n int
512 n, err = writeEscapedString(w, name, true)
513 written += n
514 if err != nil {
515 return written, err
516 }
517 err = w.WriteByte('"')
518 written++
519 return written, err
520}