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