| // Copyright 2020 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 ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "io" |
| "math" |
| "strconv" |
| "strings" |
| |
| dto "github.com/prometheus/client_model/go" |
| "google.golang.org/protobuf/types/known/timestamppb" |
| |
| "github.com/prometheus/common/model" |
| ) |
| |
| type encoderOption struct { |
| withCreatedLines bool |
| withUnit bool |
| } |
| |
| type EncoderOption func(*encoderOption) |
| |
| // WithCreatedLines is an EncoderOption that configures the OpenMetrics encoder |
| // to include _created lines (See |
| // https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#counter-1). |
| // Created timestamps can improve the accuracy of series reset detection, but |
| // come with a bandwidth cost. |
| // |
| // At the time of writing, created timestamp ingestion is still experimental in |
| // Prometheus and need to be enabled with the feature-flag |
| // `--feature-flag=created-timestamp-zero-ingestion`, and breaking changes are |
| // still possible. Therefore, it is recommended to use this feature with caution. |
| func WithCreatedLines() EncoderOption { |
| return func(t *encoderOption) { |
| t.withCreatedLines = true |
| } |
| } |
| |
| // WithUnit is an EncoderOption enabling a set unit to be written to the output |
| // and to be added to the metric name, if it's not there already, as a suffix. |
| // Without opting in this way, the unit will not be added to the metric name and, |
| // on top of that, the unit will not be passed onto the output, even if it |
| // were declared in the *dto.MetricFamily struct, i.e. even if in.Unit !=nil. |
| func WithUnit() EncoderOption { |
| return func(t *encoderOption) { |
| t.withUnit = true |
| } |
| } |
| |
| // MetricFamilyToOpenMetrics converts a MetricFamily proto message into the |
| // OpenMetrics 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. |
| // |
| // If metric names conform to the legacy validation pattern, they will be placed |
| // outside the brackets in the traditional way, like `foo{}`. If the metric name |
| // fails the legacy validation check, it will be placed quoted inside the |
| // brackets: `{"foo"}`. As stated above, the input is assumed to be santized and |
| // no error will be thrown in this case. |
| // |
| // Similar to metric names, if label names conform to the legacy validation |
| // pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label |
| // name fails the legacy validation check, it will be quoted: |
| // `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and |
| // no error will be thrown in this case. |
| // |
| // This function fulfills the type 'expfmt.encoder'. |
| // |
| // Note that OpenMetrics requires a final `# EOF` line. Since this function acts |
| // on individual metric families, it is the responsibility of the caller to |
| // append this line to 'out' once all metric families have been written. |
| // Conveniently, this can be done by calling FinalizeOpenMetrics. |
| // |
| // The output should be fully OpenMetrics compliant. However, there are a few |
| // missing features and peculiarities to avoid complications when switching from |
| // Prometheus to OpenMetrics or vice versa: |
| // |
| // - Counters are expected to have the `_total` suffix in their metric name. In |
| // the output, the suffix will be truncated from the `# TYPE`, `# HELP` and `# UNIT` |
| // lines. A counter with a missing `_total` suffix is not an error. However, |
| // its type will be set to `unknown` in that case to avoid invalid OpenMetrics |
| // output. |
| // |
| // - According to the OM specs, the `# UNIT` line is optional, but if populated, |
| // the unit has to be present in the metric name as its suffix: |
| // (see https://github.com/prometheus/OpenMetrics/blob/v1.0.0/specification/OpenMetrics.md#unit). |
| // However, in order to accommodate any potential scenario where such a change in the |
| // metric name is not desirable, the users are here given the choice of either explicitly |
| // opt in, in case they wish for the unit to be included in the output AND in the metric name |
| // as a suffix (see the description of the WithUnit function above), |
| // or not to opt in, in case they don't want for any of that to happen. |
| // |
| // - No support for the following (optional) features: info type, |
| // stateset type, gaugehistogram type. |
| // |
| // - The size of exemplar labels is not checked (i.e. it's possible to create |
| // exemplars that are larger than allowed by the OpenMetrics specification). |
| // |
| // - The value of Counters is not checked. (OpenMetrics doesn't allow counters |
| // with a `NaN` value.) |
| func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) { |
| toOM := encoderOption{} |
| for _, option := range options { |
| option(&toOM) |
| } |
| |
| name := in.GetName() |
| if name == "" { |
| return 0, fmt.Errorf("MetricFamily has no name: %s", in) |
| } |
| |
| // Try the interface upgrade. If it doesn't work, we'll use a |
| // bufio.Writer from the sync.Pool. |
| w, ok := out.(enhancedWriter) |
| if !ok { |
| b := bufPool.Get().(*bufio.Writer) |
| b.Reset(out) |
| w = b |
| defer func() { |
| bErr := b.Flush() |
| if err == nil { |
| err = bErr |
| } |
| bufPool.Put(b) |
| }() |
| } |
| |
| var ( |
| n int |
| metricType = in.GetType() |
| compliantName = name |
| ) |
| if metricType == dto.MetricType_COUNTER && strings.HasSuffix(compliantName, "_total") { |
| compliantName = name[:len(name)-6] |
| } |
| if toOM.withUnit && in.Unit != nil && !strings.HasSuffix(compliantName, "_"+*in.Unit) { |
| compliantName = compliantName + "_" + *in.Unit |
| } |
| |
| // Comments, first HELP, then TYPE. |
| if in.Help != nil { |
| n, err = w.WriteString("# HELP ") |
| written += n |
| if err != nil { |
| return |
| } |
| n, err = writeName(w, compliantName) |
| written += n |
| if err != nil { |
| return |
| } |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return |
| } |
| n, err = writeEscapedString(w, *in.Help, true) |
| written += n |
| if err != nil { |
| return |
| } |
| err = w.WriteByte('\n') |
| written++ |
| if err != nil { |
| return |
| } |
| } |
| n, err = w.WriteString("# TYPE ") |
| written += n |
| if err != nil { |
| return |
| } |
| n, err = writeName(w, compliantName) |
| written += n |
| if err != nil { |
| return |
| } |
| switch metricType { |
| case dto.MetricType_COUNTER: |
| if strings.HasSuffix(name, "_total") { |
| n, err = w.WriteString(" counter\n") |
| } else { |
| n, err = w.WriteString(" unknown\n") |
| } |
| case dto.MetricType_GAUGE: |
| n, err = w.WriteString(" gauge\n") |
| case dto.MetricType_SUMMARY: |
| n, err = w.WriteString(" summary\n") |
| case dto.MetricType_UNTYPED: |
| n, err = w.WriteString(" unknown\n") |
| case dto.MetricType_HISTOGRAM: |
| n, err = w.WriteString(" histogram\n") |
| default: |
| return written, fmt.Errorf("unknown metric type %s", metricType.String()) |
| } |
| written += n |
| if err != nil { |
| return |
| } |
| if toOM.withUnit && in.Unit != nil { |
| n, err = w.WriteString("# UNIT ") |
| written += n |
| if err != nil { |
| return |
| } |
| n, err = writeName(w, compliantName) |
| written += n |
| if err != nil { |
| return |
| } |
| |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return |
| } |
| n, err = writeEscapedString(w, *in.Unit, true) |
| written += n |
| if err != nil { |
| return |
| } |
| err = w.WriteByte('\n') |
| written++ |
| if err != nil { |
| return |
| } |
| } |
| |
| var createdTsBytesWritten int |
| |
| // Finally the samples, one line for each. |
| if metricType == dto.MetricType_COUNTER && strings.HasSuffix(name, "_total") { |
| compliantName += "_total" |
| } |
| 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", compliantName, metric, |
| ) |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "", metric, "", 0, |
| metric.Counter.GetValue(), 0, false, |
| metric.Counter.Exemplar, |
| ) |
| if toOM.withCreatedLines && metric.Counter.CreatedTimestamp != nil { |
| createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "_total", metric, "", 0, metric.Counter.GetCreatedTimestamp()) |
| n += createdTsBytesWritten |
| } |
| case dto.MetricType_GAUGE: |
| if metric.Gauge == nil { |
| return written, fmt.Errorf( |
| "expected gauge in metric %s %s", compliantName, metric, |
| ) |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "", metric, "", 0, |
| metric.Gauge.GetValue(), 0, false, |
| nil, |
| ) |
| case dto.MetricType_UNTYPED: |
| if metric.Untyped == nil { |
| return written, fmt.Errorf( |
| "expected untyped in metric %s %s", compliantName, metric, |
| ) |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "", metric, "", 0, |
| metric.Untyped.GetValue(), 0, false, |
| nil, |
| ) |
| case dto.MetricType_SUMMARY: |
| if metric.Summary == nil { |
| return written, fmt.Errorf( |
| "expected summary in metric %s %s", compliantName, metric, |
| ) |
| } |
| for _, q := range metric.Summary.Quantile { |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "", metric, |
| model.QuantileLabel, q.GetQuantile(), |
| q.GetValue(), 0, false, |
| nil, |
| ) |
| written += n |
| if err != nil { |
| return |
| } |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "_sum", metric, "", 0, |
| metric.Summary.GetSampleSum(), 0, false, |
| nil, |
| ) |
| written += n |
| if err != nil { |
| return |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "_count", metric, "", 0, |
| 0, metric.Summary.GetSampleCount(), true, |
| nil, |
| ) |
| if toOM.withCreatedLines && metric.Summary.CreatedTimestamp != nil { |
| createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Summary.GetCreatedTimestamp()) |
| n += createdTsBytesWritten |
| } |
| case dto.MetricType_HISTOGRAM: |
| if metric.Histogram == nil { |
| return written, fmt.Errorf( |
| "expected histogram in metric %s %s", compliantName, metric, |
| ) |
| } |
| infSeen := false |
| for _, b := range metric.Histogram.Bucket { |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "_bucket", metric, |
| model.BucketLabel, b.GetUpperBound(), |
| 0, b.GetCumulativeCount(), true, |
| b.Exemplar, |
| ) |
| written += n |
| if err != nil { |
| return |
| } |
| if math.IsInf(b.GetUpperBound(), +1) { |
| infSeen = true |
| } |
| } |
| if !infSeen { |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "_bucket", metric, |
| model.BucketLabel, math.Inf(+1), |
| 0, metric.Histogram.GetSampleCount(), true, |
| nil, |
| ) |
| written += n |
| if err != nil { |
| return |
| } |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "_sum", metric, "", 0, |
| metric.Histogram.GetSampleSum(), 0, false, |
| nil, |
| ) |
| written += n |
| if err != nil { |
| return |
| } |
| n, err = writeOpenMetricsSample( |
| w, compliantName, "_count", metric, "", 0, |
| 0, metric.Histogram.GetSampleCount(), true, |
| nil, |
| ) |
| if toOM.withCreatedLines && metric.Histogram.CreatedTimestamp != nil { |
| createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Histogram.GetCreatedTimestamp()) |
| n += createdTsBytesWritten |
| } |
| default: |
| return written, fmt.Errorf( |
| "unexpected type in metric %s %s", compliantName, metric, |
| ) |
| } |
| written += n |
| if err != nil { |
| return |
| } |
| } |
| return |
| } |
| |
| // FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics. |
| func FinalizeOpenMetrics(w io.Writer) (written int, err error) { |
| return w.Write([]byte("# EOF\n")) |
| } |
| |
| // writeOpenMetricsSample writes a single sample in OpenMetrics text format to |
| // w, given the metric name, the metric proto message itself, optionally an |
| // additional label name with a float64 value (use empty string as label name if |
| // not required), the value (optionally as float64 or uint64, determined by |
| // useIntValue), and optionally an exemplar (use nil if not required). The |
| // function returns the number of bytes written and any error encountered. |
| func writeOpenMetricsSample( |
| w enhancedWriter, |
| name, suffix string, |
| metric *dto.Metric, |
| additionalLabelName string, additionalLabelValue float64, |
| floatValue float64, intValue uint64, useIntValue bool, |
| exemplar *dto.Exemplar, |
| ) (int, error) { |
| written := 0 |
| n, err := writeOpenMetricsNameAndLabelPairs( |
| w, name+suffix, metric.Label, additionalLabelName, additionalLabelValue, |
| ) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| if useIntValue { |
| n, err = writeUint(w, intValue) |
| } else { |
| n, err = writeOpenMetricsFloat(w, floatValue) |
| } |
| written += n |
| if err != nil { |
| return written, err |
| } |
| if metric.TimestampMs != nil { |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| // TODO(beorn7): Format this directly without converting to a float first. |
| n, err = writeOpenMetricsFloat(w, float64(*metric.TimestampMs)/1000) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| } |
| if exemplar != nil && len(exemplar.Label) > 0 { |
| n, err = writeExemplar(w, exemplar) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| } |
| err = w.WriteByte('\n') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| return written, nil |
| } |
| |
| // writeOpenMetricsNameAndLabelPairs works like writeOpenMetricsSample but |
| // formats the float in OpenMetrics style. |
| func writeOpenMetricsNameAndLabelPairs( |
| w enhancedWriter, |
| name string, |
| in []*dto.LabelPair, |
| additionalLabelName string, additionalLabelValue float64, |
| ) (int, error) { |
| var ( |
| written int |
| separator byte = '{' |
| metricInsideBraces = false |
| ) |
| |
| if name != "" { |
| // If the name does not pass the legacy validity check, we must put the |
| // metric name inside the braces, quoted. |
| if !model.LegacyValidation.IsValidMetricName(name) { |
| metricInsideBraces = true |
| err := w.WriteByte(separator) |
| written++ |
| if err != nil { |
| return written, err |
| } |
| separator = ',' |
| } |
| |
| n, err := writeName(w, name) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| } |
| |
| if len(in) == 0 && additionalLabelName == "" { |
| if metricInsideBraces { |
| err := w.WriteByte('}') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| } |
| return written, nil |
| } |
| |
| for _, lp := range in { |
| err := w.WriteByte(separator) |
| written++ |
| if err != nil { |
| return written, err |
| } |
| n, err := writeName(w, lp.GetName()) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| n, err = w.WriteString(`="`) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| n, err = writeEscapedString(w, lp.GetValue(), true) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| err = w.WriteByte('"') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| separator = ',' |
| } |
| if additionalLabelName != "" { |
| err := w.WriteByte(separator) |
| written++ |
| if err != nil { |
| return written, err |
| } |
| n, err := w.WriteString(additionalLabelName) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| n, err = w.WriteString(`="`) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| n, err = writeOpenMetricsFloat(w, additionalLabelValue) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| err = w.WriteByte('"') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| } |
| err := w.WriteByte('}') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| return written, nil |
| } |
| |
| // writeOpenMetricsCreated writes the created timestamp for a single time series |
| // following OpenMetrics text format to w, given the metric name, the metric proto |
| // message itself, optionally a suffix to be removed, e.g. '_total' for counters, |
| // an additional label name with a float64 value (use empty string as label name if |
| // not required) and the timestamp that represents the created timestamp. |
| // The function returns the number of bytes written and any error encountered. |
| func writeOpenMetricsCreated(w enhancedWriter, |
| name, suffixToTrim string, metric *dto.Metric, |
| additionalLabelName string, additionalLabelValue float64, |
| createdTimestamp *timestamppb.Timestamp, |
| ) (int, error) { |
| written := 0 |
| n, err := writeOpenMetricsNameAndLabelPairs( |
| w, strings.TrimSuffix(name, suffixToTrim)+"_created", metric.Label, additionalLabelName, additionalLabelValue, |
| ) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| |
| // TODO(beorn7): Format this directly from components of ts to |
| // avoid overflow/underflow and precision issues of the float |
| // conversion. |
| n, err = writeOpenMetricsFloat(w, float64(createdTimestamp.AsTime().UnixNano())/1e9) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| |
| err = w.WriteByte('\n') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| return written, nil |
| } |
| |
| // writeExemplar writes the provided exemplar in OpenMetrics format to w. The |
| // function returns the number of bytes written and any error encountered. |
| func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) { |
| written := 0 |
| n, err := w.WriteString(" # ") |
| written += n |
| if err != nil { |
| return written, err |
| } |
| n, err = writeOpenMetricsNameAndLabelPairs(w, "", e.Label, "", 0) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| n, err = writeOpenMetricsFloat(w, e.GetValue()) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| if e.Timestamp != nil { |
| err = w.WriteByte(' ') |
| written++ |
| if err != nil { |
| return written, err |
| } |
| err = e.Timestamp.CheckValid() |
| if err != nil { |
| return written, err |
| } |
| ts := e.Timestamp.AsTime() |
| // TODO(beorn7): Format this directly from components of ts to |
| // avoid overflow/underflow and precision issues of the float |
| // conversion. |
| n, err = writeOpenMetricsFloat(w, float64(ts.UnixNano())/1e9) |
| written += n |
| if err != nil { |
| return written, err |
| } |
| } |
| return written, nil |
| } |
| |
| // writeOpenMetricsFloat works like writeFloat but appends ".0" if the resulting |
| // number would otherwise contain neither a "." nor an "e". |
| func writeOpenMetricsFloat(w enhancedWriter, f float64) (int, error) { |
| switch { |
| case f == 1: |
| return w.WriteString("1.0") |
| case f == 0: |
| return w.WriteString("0.0") |
| case f == -1: |
| return w.WriteString("-1.0") |
| case math.IsNaN(f): |
| return w.WriteString("NaN") |
| case math.IsInf(f, +1): |
| return w.WriteString("+Inf") |
| case math.IsInf(f, -1): |
| return w.WriteString("-Inf") |
| default: |
| bp := numBufPool.Get().(*[]byte) |
| *bp = strconv.AppendFloat((*bp)[:0], f, 'g', -1, 64) |
| if !bytes.ContainsAny(*bp, "e.") { |
| *bp = append(*bp, '.', '0') |
| } |
| written, err := w.Write(*bp) |
| numBufPool.Put(bp) |
| return written, err |
| } |
| } |
| |
| // writeUint is like writeInt just for uint64. |
| func writeUint(w enhancedWriter, u uint64) (int, error) { |
| bp := numBufPool.Get().(*[]byte) |
| *bp = strconv.AppendUint((*bp)[:0], u, 10) |
| written, err := w.Write(*bp) |
| numBufPool.Put(bp) |
| return written, err |
| } |