mirror of https://github.com/hashicorp/consul
304 lines
7.5 KiB
Go
304 lines
7.5 KiB
Go
|
// Copyright 2014 The Prometheus Authors
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
package expfmt
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math"
|
||
|
"strings"
|
||
|
|
||
|
dto "github.com/prometheus/client_model/go"
|
||
|
"github.com/prometheus/common/model"
|
||
|
)
|
||
|
|
||
|
// MetricFamilyToText converts a MetricFamily proto message into text format and
|
||
|
// writes the resulting lines to 'out'. It returns the number of bytes written
|
||
|
// and any error encountered. The output will have the same order as the input,
|
||
|
// no further sorting is performed. Furthermore, this function assumes the input
|
||
|
// is already sanitized and does not perform any sanity checks. If the input
|
||
|
// contains duplicate metrics or invalid metric or label names, the conversion
|
||
|
// will result in invalid text format output.
|
||
|
//
|
||
|
// This method fulfills the type 'prometheus.encoder'.
|
||
|
func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (int, error) {
|
||
|
var written int
|
||
|
|
||
|
// Fail-fast checks.
|
||
|
if len(in.Metric) == 0 {
|
||
|
return written, fmt.Errorf("MetricFamily has no metrics: %s", in)
|
||
|
}
|
||
|
name := in.GetName()
|
||
|
if name == "" {
|
||
|
return written, fmt.Errorf("MetricFamily has no name: %s", in)
|
||
|
}
|
||
|
|
||
|
// Comments, first HELP, then TYPE.
|
||
|
if in.Help != nil {
|
||
|
n, err := fmt.Fprintf(
|
||
|
out, "# HELP %s %s\n",
|
||
|
name, escapeString(*in.Help, false),
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
}
|
||
|
metricType := in.GetType()
|
||
|
n, err := fmt.Fprintf(
|
||
|
out, "# TYPE %s %s\n",
|
||
|
name, strings.ToLower(metricType.String()),
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
|
||
|
// Finally the samples, one line for each.
|
||
|
for _, metric := range in.Metric {
|
||
|
switch metricType {
|
||
|
case dto.MetricType_COUNTER:
|
||
|
if metric.Counter == nil {
|
||
|
return written, fmt.Errorf(
|
||
|
"expected counter in metric %s %s", name, metric,
|
||
|
)
|
||
|
}
|
||
|
n, err = writeSample(
|
||
|
name, metric, "", "",
|
||
|
metric.Counter.GetValue(),
|
||
|
out,
|
||
|
)
|
||
|
case dto.MetricType_GAUGE:
|
||
|
if metric.Gauge == nil {
|
||
|
return written, fmt.Errorf(
|
||
|
"expected gauge in metric %s %s", name, metric,
|
||
|
)
|
||
|
}
|
||
|
n, err = writeSample(
|
||
|
name, metric, "", "",
|
||
|
metric.Gauge.GetValue(),
|
||
|
out,
|
||
|
)
|
||
|
case dto.MetricType_UNTYPED:
|
||
|
if metric.Untyped == nil {
|
||
|
return written, fmt.Errorf(
|
||
|
"expected untyped in metric %s %s", name, metric,
|
||
|
)
|
||
|
}
|
||
|
n, err = writeSample(
|
||
|
name, metric, "", "",
|
||
|
metric.Untyped.GetValue(),
|
||
|
out,
|
||
|
)
|
||
|
case dto.MetricType_SUMMARY:
|
||
|
if metric.Summary == nil {
|
||
|
return written, fmt.Errorf(
|
||
|
"expected summary in metric %s %s", name, metric,
|
||
|
)
|
||
|
}
|
||
|
for _, q := range metric.Summary.Quantile {
|
||
|
n, err = writeSample(
|
||
|
name, metric,
|
||
|
model.QuantileLabel, fmt.Sprint(q.GetQuantile()),
|
||
|
q.GetValue(),
|
||
|
out,
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
}
|
||
|
n, err = writeSample(
|
||
|
name+"_sum", metric, "", "",
|
||
|
metric.Summary.GetSampleSum(),
|
||
|
out,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
written += n
|
||
|
n, err = writeSample(
|
||
|
name+"_count", metric, "", "",
|
||
|
float64(metric.Summary.GetSampleCount()),
|
||
|
out,
|
||
|
)
|
||
|
case dto.MetricType_HISTOGRAM:
|
||
|
if metric.Histogram == nil {
|
||
|
return written, fmt.Errorf(
|
||
|
"expected histogram in metric %s %s", name, metric,
|
||
|
)
|
||
|
}
|
||
|
infSeen := false
|
||
|
for _, q := range metric.Histogram.Bucket {
|
||
|
n, err = writeSample(
|
||
|
name+"_bucket", metric,
|
||
|
model.BucketLabel, fmt.Sprint(q.GetUpperBound()),
|
||
|
float64(q.GetCumulativeCount()),
|
||
|
out,
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
if math.IsInf(q.GetUpperBound(), +1) {
|
||
|
infSeen = true
|
||
|
}
|
||
|
}
|
||
|
if !infSeen {
|
||
|
n, err = writeSample(
|
||
|
name+"_bucket", metric,
|
||
|
model.BucketLabel, "+Inf",
|
||
|
float64(metric.Histogram.GetSampleCount()),
|
||
|
out,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
written += n
|
||
|
}
|
||
|
n, err = writeSample(
|
||
|
name+"_sum", metric, "", "",
|
||
|
metric.Histogram.GetSampleSum(),
|
||
|
out,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
written += n
|
||
|
n, err = writeSample(
|
||
|
name+"_count", metric, "", "",
|
||
|
float64(metric.Histogram.GetSampleCount()),
|
||
|
out,
|
||
|
)
|
||
|
default:
|
||
|
return written, fmt.Errorf(
|
||
|
"unexpected type in metric %s %s", name, metric,
|
||
|
)
|
||
|
}
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
}
|
||
|
return written, nil
|
||
|
}
|
||
|
|
||
|
// writeSample writes a single sample in text format to out, given the metric
|
||
|
// name, the metric proto message itself, optionally an additional label name
|
||
|
// and value (use empty strings if not required), and the value. The function
|
||
|
// returns the number of bytes written and any error encountered.
|
||
|
func writeSample(
|
||
|
name string,
|
||
|
metric *dto.Metric,
|
||
|
additionalLabelName, additionalLabelValue string,
|
||
|
value float64,
|
||
|
out io.Writer,
|
||
|
) (int, error) {
|
||
|
var written int
|
||
|
n, err := fmt.Fprint(out, name)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
n, err = labelPairsToText(
|
||
|
metric.Label,
|
||
|
additionalLabelName, additionalLabelValue,
|
||
|
out,
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
n, err = fmt.Fprintf(out, " %v", value)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
if metric.TimestampMs != nil {
|
||
|
n, err = fmt.Fprintf(out, " %v", *metric.TimestampMs)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
}
|
||
|
n, err = out.Write([]byte{'\n'})
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
return written, nil
|
||
|
}
|
||
|
|
||
|
// labelPairsToText converts a slice of LabelPair proto messages plus the
|
||
|
// explicitly given additional label pair into text formatted as required by the
|
||
|
// text format and writes it to 'out'. An empty slice in combination with an
|
||
|
// empty string 'additionalLabelName' results in nothing being
|
||
|
// written. Otherwise, the label pairs are written, escaped as required by the
|
||
|
// text format, and enclosed in '{...}'. The function returns the number of
|
||
|
// bytes written and any error encountered.
|
||
|
func labelPairsToText(
|
||
|
in []*dto.LabelPair,
|
||
|
additionalLabelName, additionalLabelValue string,
|
||
|
out io.Writer,
|
||
|
) (int, error) {
|
||
|
if len(in) == 0 && additionalLabelName == "" {
|
||
|
return 0, nil
|
||
|
}
|
||
|
var written int
|
||
|
separator := '{'
|
||
|
for _, lp := range in {
|
||
|
n, err := fmt.Fprintf(
|
||
|
out, `%c%s="%s"`,
|
||
|
separator, lp.GetName(), escapeString(lp.GetValue(), true),
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
separator = ','
|
||
|
}
|
||
|
if additionalLabelName != "" {
|
||
|
n, err := fmt.Fprintf(
|
||
|
out, `%c%s="%s"`,
|
||
|
separator, additionalLabelName,
|
||
|
escapeString(additionalLabelValue, true),
|
||
|
)
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
}
|
||
|
n, err := out.Write([]byte{'}'})
|
||
|
written += n
|
||
|
if err != nil {
|
||
|
return written, err
|
||
|
}
|
||
|
return written, nil
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
escape = strings.NewReplacer("\\", `\\`, "\n", `\n`)
|
||
|
escapeWithDoubleQuote = strings.NewReplacer("\\", `\\`, "\n", `\n`, "\"", `\"`)
|
||
|
)
|
||
|
|
||
|
// escapeString replaces '\' by '\\', new line character by '\n', and - if
|
||
|
// includeDoubleQuote is true - '"' by '\"'.
|
||
|
func escapeString(v string, includeDoubleQuote bool) string {
|
||
|
if includeDoubleQuote {
|
||
|
return escapeWithDoubleQuote.Replace(v)
|
||
|
}
|
||
|
|
||
|
return escape.Replace(v)
|
||
|
}
|