1// 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}
528