blob: 8a9313a3bee9c4eb9d8b5fe2624dab470b4de508 [file] [log] [blame]
khenaidoo26721882021-08-11 17:42:52 -04001// Copyright 2020 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 (
17 "bufio"
18 "bytes"
19 "fmt"
20 "io"
21 "math"
22 "strconv"
23 "strings"
24
25 "github.com/golang/protobuf/ptypes"
26 "github.com/prometheus/common/model"
27
28 dto "github.com/prometheus/client_model/go"
29)
30
31// MetricFamilyToOpenMetrics converts a MetricFamily proto message into the
32// OpenMetrics text format and writes the resulting lines to 'out'. It returns
33// the number of bytes written and any error encountered. The output will have
34// the same order as the input, no further sorting is performed. Furthermore,
35// this function assumes the input is already sanitized and does not perform any
36// sanity checks. If the input contains duplicate metrics or invalid metric or
37// label names, the conversion will result in invalid text format output.
38//
39// This function fulfills the type 'expfmt.encoder'.
40//
41// Note that OpenMetrics requires a final `# EOF` line. Since this function acts
42// on individual metric families, it is the responsibility of the caller to
43// append this line to 'out' once all metric families have been written.
44// Conveniently, this can be done by calling FinalizeOpenMetrics.
45//
46// The output should be fully OpenMetrics compliant. However, there are a few
47// missing features and peculiarities to avoid complications when switching from
48// Prometheus to OpenMetrics or vice versa:
49//
50// - Counters are expected to have the `_total` suffix in their metric name. In
51// the output, the suffix will be truncated from the `# TYPE` and `# HELP`
52// line. A counter with a missing `_total` suffix is not an error. However,
53// its type will be set to `unknown` in that case to avoid invalid OpenMetrics
54// output.
55//
56// - No support for the following (optional) features: `# UNIT` line, `_created`
57// line, info type, stateset type, gaugehistogram type.
58//
59// - The size of exemplar labels is not checked (i.e. it's possible to create
60// exemplars that are larger than allowed by the OpenMetrics specification).
61//
62// - The value of Counters is not checked. (OpenMetrics doesn't allow counters
63// with a `NaN` value.)
64func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int, err error) {
65 name := in.GetName()
66 if name == "" {
67 return 0, fmt.Errorf("MetricFamily has no name: %s", in)
68 }
69
70 // Try the interface upgrade. If it doesn't work, we'll use a
71 // bufio.Writer from the sync.Pool.
72 w, ok := out.(enhancedWriter)
73 if !ok {
74 b := bufPool.Get().(*bufio.Writer)
75 b.Reset(out)
76 w = b
77 defer func() {
78 bErr := b.Flush()
79 if err == nil {
80 err = bErr
81 }
82 bufPool.Put(b)
83 }()
84 }
85
86 var (
87 n int
88 metricType = in.GetType()
89 shortName = name
90 )
91 if metricType == dto.MetricType_COUNTER && strings.HasSuffix(shortName, "_total") {
92 shortName = name[:len(name)-6]
93 }
94
95 // Comments, first HELP, then TYPE.
96 if in.Help != nil {
97 n, err = w.WriteString("# HELP ")
98 written += n
99 if err != nil {
100 return
101 }
102 n, err = w.WriteString(shortName)
103 written += n
104 if err != nil {
105 return
106 }
107 err = w.WriteByte(' ')
108 written++
109 if err != nil {
110 return
111 }
112 n, err = writeEscapedString(w, *in.Help, true)
113 written += n
114 if err != nil {
115 return
116 }
117 err = w.WriteByte('\n')
118 written++
119 if err != nil {
120 return
121 }
122 }
123 n, err = w.WriteString("# TYPE ")
124 written += n
125 if err != nil {
126 return
127 }
128 n, err = w.WriteString(shortName)
129 written += n
130 if err != nil {
131 return
132 }
133 switch metricType {
134 case dto.MetricType_COUNTER:
135 if strings.HasSuffix(name, "_total") {
136 n, err = w.WriteString(" counter\n")
137 } else {
138 n, err = w.WriteString(" unknown\n")
139 }
140 case dto.MetricType_GAUGE:
141 n, err = w.WriteString(" gauge\n")
142 case dto.MetricType_SUMMARY:
143 n, err = w.WriteString(" summary\n")
144 case dto.MetricType_UNTYPED:
145 n, err = w.WriteString(" unknown\n")
146 case dto.MetricType_HISTOGRAM:
147 n, err = w.WriteString(" histogram\n")
148 default:
149 return written, fmt.Errorf("unknown metric type %s", metricType.String())
150 }
151 written += n
152 if err != nil {
153 return
154 }
155
156 // Finally the samples, one line for each.
157 for _, metric := range in.Metric {
158 switch metricType {
159 case dto.MetricType_COUNTER:
160 if metric.Counter == nil {
161 return written, fmt.Errorf(
162 "expected counter in metric %s %s", name, metric,
163 )
164 }
165 // Note that we have ensured above that either the name
166 // ends on `_total` or that the rendered type is
167 // `unknown`. Therefore, no `_total` must be added here.
168 n, err = writeOpenMetricsSample(
169 w, name, "", metric, "", 0,
170 metric.Counter.GetValue(), 0, false,
171 metric.Counter.Exemplar,
172 )
173 case dto.MetricType_GAUGE:
174 if metric.Gauge == nil {
175 return written, fmt.Errorf(
176 "expected gauge in metric %s %s", name, metric,
177 )
178 }
179 n, err = writeOpenMetricsSample(
180 w, name, "", metric, "", 0,
181 metric.Gauge.GetValue(), 0, false,
182 nil,
183 )
184 case dto.MetricType_UNTYPED:
185 if metric.Untyped == nil {
186 return written, fmt.Errorf(
187 "expected untyped in metric %s %s", name, metric,
188 )
189 }
190 n, err = writeOpenMetricsSample(
191 w, name, "", metric, "", 0,
192 metric.Untyped.GetValue(), 0, false,
193 nil,
194 )
195 case dto.MetricType_SUMMARY:
196 if metric.Summary == nil {
197 return written, fmt.Errorf(
198 "expected summary in metric %s %s", name, metric,
199 )
200 }
201 for _, q := range metric.Summary.Quantile {
202 n, err = writeOpenMetricsSample(
203 w, name, "", metric,
204 model.QuantileLabel, q.GetQuantile(),
205 q.GetValue(), 0, false,
206 nil,
207 )
208 written += n
209 if err != nil {
210 return
211 }
212 }
213 n, err = writeOpenMetricsSample(
214 w, name, "_sum", metric, "", 0,
215 metric.Summary.GetSampleSum(), 0, false,
216 nil,
217 )
218 written += n
219 if err != nil {
220 return
221 }
222 n, err = writeOpenMetricsSample(
223 w, name, "_count", metric, "", 0,
224 0, metric.Summary.GetSampleCount(), true,
225 nil,
226 )
227 case dto.MetricType_HISTOGRAM:
228 if metric.Histogram == nil {
229 return written, fmt.Errorf(
230 "expected histogram in metric %s %s", name, metric,
231 )
232 }
233 infSeen := false
234 for _, b := range metric.Histogram.Bucket {
235 n, err = writeOpenMetricsSample(
236 w, name, "_bucket", metric,
237 model.BucketLabel, b.GetUpperBound(),
238 0, b.GetCumulativeCount(), true,
239 b.Exemplar,
240 )
241 written += n
242 if err != nil {
243 return
244 }
245 if math.IsInf(b.GetUpperBound(), +1) {
246 infSeen = true
247 }
248 }
249 if !infSeen {
250 n, err = writeOpenMetricsSample(
251 w, name, "_bucket", metric,
252 model.BucketLabel, math.Inf(+1),
253 0, metric.Histogram.GetSampleCount(), true,
254 nil,
255 )
256 written += n
257 if err != nil {
258 return
259 }
260 }
261 n, err = writeOpenMetricsSample(
262 w, name, "_sum", metric, "", 0,
263 metric.Histogram.GetSampleSum(), 0, false,
264 nil,
265 )
266 written += n
267 if err != nil {
268 return
269 }
270 n, err = writeOpenMetricsSample(
271 w, name, "_count", metric, "", 0,
272 0, metric.Histogram.GetSampleCount(), true,
273 nil,
274 )
275 default:
276 return written, fmt.Errorf(
277 "unexpected type in metric %s %s", name, metric,
278 )
279 }
280 written += n
281 if err != nil {
282 return
283 }
284 }
285 return
286}
287
288// FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics.
289func FinalizeOpenMetrics(w io.Writer) (written int, err error) {
290 return w.Write([]byte("# EOF\n"))
291}
292
293// writeOpenMetricsSample writes a single sample in OpenMetrics text format to
294// w, given the metric name, the metric proto message itself, optionally an
295// additional label name with a float64 value (use empty string as label name if
296// not required), the value (optionally as float64 or uint64, determined by
297// useIntValue), and optionally an exemplar (use nil if not required). The
298// function returns the number of bytes written and any error encountered.
299func writeOpenMetricsSample(
300 w enhancedWriter,
301 name, suffix string,
302 metric *dto.Metric,
303 additionalLabelName string, additionalLabelValue float64,
304 floatValue float64, intValue uint64, useIntValue bool,
305 exemplar *dto.Exemplar,
306) (int, error) {
307 var written int
308 n, err := w.WriteString(name)
309 written += n
310 if err != nil {
311 return written, err
312 }
313 if suffix != "" {
314 n, err = w.WriteString(suffix)
315 written += n
316 if err != nil {
317 return written, err
318 }
319 }
320 n, err = writeOpenMetricsLabelPairs(
321 w, metric.Label, additionalLabelName, additionalLabelValue,
322 )
323 written += n
324 if err != nil {
325 return written, err
326 }
327 err = w.WriteByte(' ')
328 written++
329 if err != nil {
330 return written, err
331 }
332 if useIntValue {
333 n, err = writeUint(w, intValue)
334 } else {
335 n, err = writeOpenMetricsFloat(w, floatValue)
336 }
337 written += n
338 if err != nil {
339 return written, err
340 }
341 if metric.TimestampMs != nil {
342 err = w.WriteByte(' ')
343 written++
344 if err != nil {
345 return written, err
346 }
347 // TODO(beorn7): Format this directly without converting to a float first.
348 n, err = writeOpenMetricsFloat(w, float64(*metric.TimestampMs)/1000)
349 written += n
350 if err != nil {
351 return written, err
352 }
353 }
354 if exemplar != nil {
355 n, err = writeExemplar(w, exemplar)
356 written += n
357 if err != nil {
358 return written, err
359 }
360 }
361 err = w.WriteByte('\n')
362 written++
363 if err != nil {
364 return written, err
365 }
366 return written, nil
367}
368
369// writeOpenMetricsLabelPairs works like writeOpenMetrics but formats the float
370// in OpenMetrics style.
371func writeOpenMetricsLabelPairs(
372 w enhancedWriter,
373 in []*dto.LabelPair,
374 additionalLabelName string, additionalLabelValue float64,
375) (int, error) {
376 if len(in) == 0 && additionalLabelName == "" {
377 return 0, nil
378 }
379 var (
380 written int
381 separator byte = '{'
382 )
383 for _, lp := range in {
384 err := w.WriteByte(separator)
385 written++
386 if err != nil {
387 return written, err
388 }
389 n, err := w.WriteString(lp.GetName())
390 written += n
391 if err != nil {
392 return written, err
393 }
394 n, err = w.WriteString(`="`)
395 written += n
396 if err != nil {
397 return written, err
398 }
399 n, err = writeEscapedString(w, lp.GetValue(), true)
400 written += n
401 if err != nil {
402 return written, err
403 }
404 err = w.WriteByte('"')
405 written++
406 if err != nil {
407 return written, err
408 }
409 separator = ','
410 }
411 if additionalLabelName != "" {
412 err := w.WriteByte(separator)
413 written++
414 if err != nil {
415 return written, err
416 }
417 n, err := w.WriteString(additionalLabelName)
418 written += n
419 if err != nil {
420 return written, err
421 }
422 n, err = w.WriteString(`="`)
423 written += n
424 if err != nil {
425 return written, err
426 }
427 n, err = writeOpenMetricsFloat(w, additionalLabelValue)
428 written += n
429 if err != nil {
430 return written, err
431 }
432 err = w.WriteByte('"')
433 written++
434 if err != nil {
435 return written, err
436 }
437 }
438 err := w.WriteByte('}')
439 written++
440 if err != nil {
441 return written, err
442 }
443 return written, nil
444}
445
446// writeExemplar writes the provided exemplar in OpenMetrics format to w. The
447// function returns the number of bytes written and any error encountered.
448func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) {
449 written := 0
450 n, err := w.WriteString(" # ")
451 written += n
452 if err != nil {
453 return written, err
454 }
455 n, err = writeOpenMetricsLabelPairs(w, e.Label, "", 0)
456 written += n
457 if err != nil {
458 return written, err
459 }
460 err = w.WriteByte(' ')
461 written++
462 if err != nil {
463 return written, err
464 }
465 n, err = writeOpenMetricsFloat(w, e.GetValue())
466 written += n
467 if err != nil {
468 return written, err
469 }
470 if e.Timestamp != nil {
471 err = w.WriteByte(' ')
472 written++
473 if err != nil {
474 return written, err
475 }
476 ts, err := ptypes.Timestamp((*e).Timestamp)
477 if err != nil {
478 return written, err
479 }
480 // TODO(beorn7): Format this directly from components of ts to
481 // avoid overflow/underflow and precision issues of the float
482 // conversion.
483 n, err = writeOpenMetricsFloat(w, float64(ts.UnixNano())/1e9)
484 written += n
485 if err != nil {
486 return written, err
487 }
488 }
489 return written, nil
490}
491
492// writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting
493// number would otherwise contain neither a "." nor an "e".
494func writeOpenMetricsFloat(w enhancedWriter, f float64) (int, error) {
495 switch {
496 case f == 1:
497 return w.WriteString("1.0")
498 case f == 0:
499 return w.WriteString("0.0")
500 case f == -1:
501 return w.WriteString("-1.0")
502 case math.IsNaN(f):
503 return w.WriteString("NaN")
504 case math.IsInf(f, +1):
505 return w.WriteString("+Inf")
506 case math.IsInf(f, -1):
507 return w.WriteString("-Inf")
508 default:
509 bp := numBufPool.Get().(*[]byte)
510 *bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64)
511 if !bytes.ContainsAny(*bp, "e.") {
512 *bp = append(*bp, '.', '0')
513 }
514 written, err := w.Write(*bp)
515 numBufPool.Put(bp)
516 return written, err
517 }
518}
519
520// writeUint is like writeInt just for uint64.
521func writeUint(w enhancedWriter, u uint64) (int, error) {
522 bp := numBufPool.Get().(*[]byte)
523 *bp = strconv.AppendUint((*bp)[:0], u, 10)
524 written, err := w.Write(*bp)
525 numBufPool.Put(bp)
526 return written, err
527}