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