1// 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 (
17	"fmt"
18	"io"
19	"math"
20	"strings"
21
22	dto "github.com/prometheus/client_model/go"
23	"github.com/prometheus/common/model"
24)
25
26// MetricFamilyToText converts a MetricFamily proto message into text format and
27// writes the resulting lines to 'out'. It returns the number of bytes written
28// and any error encountered. The output will have the same order as the input,
29// no further sorting is performed. Furthermore, this function assumes the input
30// is already sanitized and does not perform any sanity checks. If the input
31// contains duplicate metrics or invalid metric or label names, the conversion
32// will result in invalid text format output.
33//
34// This method fulfills the type 'prometheus.encoder'.
35func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
36	var written int
37
38	// Fail-fast checks.
39	if len(in.Metric) == 0 {
40		return written, fmt.Errorf("MetricFamily has no metrics: %s", in)
41	}
42	name := in.GetName()
43	if name == "" {
44		return written, fmt.Errorf("MetricFamily has no name: %s", in)
45	}
46
47	// Comments, first HELP, then TYPE.
48	if in.Help != nil {
49		n, err := fmt.Fprintf(
50			out, "# HELP %s %s\n",
51			name, escapeString(*in.Help, false),
52		)
53		written += n
54		if err != nil {
55			return written, err
56		}
57	}
58	metricType := in.GetType()
59	n, err := fmt.Fprintf(
60		out, "# TYPE %s %s\n",
61		name, strings.ToLower(metricType.String()),
62	)
63	written += n
64	if err != nil {
65		return written, err
66	}
67
68	// Finally the samples, one line for each.
69	for _, metric := range in.Metric {
70		switch metricType {
71		case dto.MetricType_COUNTER:
72			if metric.Counter == nil {
73				return written, fmt.Errorf(
74					"expected counter in metric %s %s", name, metric,
75				)
76			}
77			n, err = writeSample(
78				name, metric, "", "",
79				metric.Counter.GetValue(),
80				out,
81			)
82		case dto.MetricType_GAUGE:
83			if metric.Gauge == nil {
84				return written, fmt.Errorf(
85					"expected gauge in metric %s %s", name, metric,
86				)
87			}
88			n, err = writeSample(
89				name, metric, "", "",
90				metric.Gauge.GetValue(),
91				out,
92			)
93		case dto.MetricType_UNTYPED:
94			if metric.Untyped == nil {
95				return written, fmt.Errorf(
96					"expected untyped in metric %s %s", name, metric,
97				)
98			}
99			n, err = writeSample(
100				name, metric, "", "",
101				metric.Untyped.GetValue(),
102				out,
103			)
104		case dto.MetricType_SUMMARY:
105			if metric.Summary == nil {
106				return written, fmt.Errorf(
107					"expected summary in metric %s %s", name, metric,
108				)
109			}
110			for _, q := range metric.Summary.Quantile {
111				n, err = writeSample(
112					name, metric,
113					model.QuantileLabel, fmt.Sprint(q.GetQuantile()),
114					q.GetValue(),
115					out,
116				)
117				written += n
118				if err != nil {
119					return written, err
120				}
121			}
122			n, err = writeSample(
123				name+"_sum", metric, "", "",
124				metric.Summary.GetSampleSum(),
125				out,
126			)
127			if err != nil {
128				return written, err
129			}
130			written += n
131			n, err = writeSample(
132				name+"_count", metric, "", "",
133				float64(metric.Summary.GetSampleCount()),
134				out,
135			)
136		case dto.MetricType_HISTOGRAM:
137			if metric.Histogram == nil {
138				return written, fmt.Errorf(
139					"expected histogram in metric %s %s", name, metric,
140				)
141			}
142			infSeen := false
143			for _, q := range metric.Histogram.Bucket {
144				n, err = writeSample(
145					name+"_bucket", metric,
146					model.BucketLabel, fmt.Sprint(q.GetUpperBound()),
147					float64(q.GetCumulativeCount()),
148					out,
149				)
150				written += n
151				if err != nil {
152					return written, err
153				}
154				if math.IsInf(q.GetUpperBound(), +1) {
155					infSeen = true
156				}
157			}
158			if !infSeen {
159				n, err = writeSample(
160					name+"_bucket", metric,
161					model.BucketLabel, "+Inf",
162					float64(metric.Histogram.GetSampleCount()),
163					out,
164				)
165				if err != nil {
166					return written, err
167				}
168				written += n
169			}
170			n, err = writeSample(
171				name+"_sum", metric, "", "",
172				metric.Histogram.GetSampleSum(),
173				out,
174			)
175			if err != nil {
176				return written, err
177			}
178			written += n
179			n, err = writeSample(
180				name+"_count", metric, "", "",
181				float64(metric.Histogram.GetSampleCount()),
182				out,
183			)
184		default:
185			return written, fmt.Errorf(
186				"unexpected type in metric %s %s", name, metric,
187			)
188		}
189		written += n
190		if err != nil {
191			return written, err
192		}
193	}
194	return written, nil
195}
196
197// writeSample writes a single sample in text format to out, given the metric
198// name, the metric proto message itself, optionally an additional label name
199// and value (use empty strings if not required), and the value. The function
200// returns the number of bytes written and any error encountered.
201func writeSample(
202	name string,
203	metric *dto.Metric,
204	additionalLabelName, additionalLabelValue string,
205	value float64,
206	out io.Writer,
207) (int, error) {
208	var written int
209	n, err := fmt.Fprint(out, name)
210	written += n
211	if err != nil {
212		return written, err
213	}
214	n, err = labelPairsToText(
215		metric.Label,
216		additionalLabelName, additionalLabelValue,
217		out,
218	)
219	written += n
220	if err != nil {
221		return written, err
222	}
223	n, err = fmt.Fprintf(out, " %v", value)
224	written += n
225	if err != nil {
226		return written, err
227	}
228	if metric.TimestampMs != nil {
229		n, err = fmt.Fprintf(out, " %v", *metric.TimestampMs)
230		written += n
231		if err != nil {
232			return written, err
233		}
234	}
235	n, err = out.Write([]byte{'\n'})
236	written += n
237	if err != nil {
238		return written, err
239	}
240	return written, nil
241}
242
243// labelPairsToText converts a slice of LabelPair proto messages plus the
244// explicitly given additional label pair into text formatted as required by the
245// text format and writes it to 'out'. An empty slice in combination with an
246// empty string 'additionalLabelName' results in nothing being
247// written. Otherwise, the label pairs are written, escaped as required by the
248// text format, and enclosed in '{...}'. The function returns the number of
249// bytes written and any error encountered.
250func labelPairsToText(
251	in []*dto.LabelPair,
252	additionalLabelName, additionalLabelValue string,
253	out io.Writer,
254) (int, error) {
255	if len(in) == 0 && additionalLabelName == "" {
256		return 0, nil
257	}
258	var written int
259	separator := '{'
260	for _, lp := range in {
261		n, err := fmt.Fprintf(
262			out, `%c%s="%s"`,
263			separator, lp.GetName(), escapeString(lp.GetValue(), true),
264		)
265		written += n
266		if err != nil {
267			return written, err
268		}
269		separator = ','
270	}
271	if additionalLabelName != "" {
272		n, err := fmt.Fprintf(
273			out, `%c%s="%s"`,
274			separator, additionalLabelName,
275			escapeString(additionalLabelValue, true),
276		)
277		written += n
278		if err != nil {
279			return written, err
280		}
281	}
282	n, err := out.Write([]byte{'}'})
283	written += n
284	if err != nil {
285		return written, err
286	}
287	return written, nil
288}
289
290var (
291	escape                = strings.NewReplacer("\\", `\\`, "\n", `\n`)
292	escapeWithDoubleQuote = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)
293)
294
295// escapeString replaces '\' by '\\', new line character by '\n', and - if
296// includeDoubleQuote is true - '"' by '\"'.
297func escapeString(v string, includeDoubleQuote bool) string {
298	if includeDoubleQuote {
299		return escapeWithDoubleQuote.Replace(v)
300	}
301
302	return escape.Replace(v)
303}
304