Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion expfmt/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/munnerz/goautoneg"
dto "github.com/prometheus/client_model/go"
Expand Down Expand Up @@ -118,8 +119,10 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format {
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
return FmtText + escapingScheme
}
if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") {
if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == OpenMetricsVersion_2_0_0 || ver == "") {
switch ver {
case OpenMetricsVersion_2_0_0:
return FmtOpenMetrics_2_0_0 + escapingScheme
case OpenMetricsVersion_1_0_0:
return FmtOpenMetrics_1_0_0 + escapingScheme
default:
Expand Down Expand Up @@ -181,6 +184,18 @@ func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder {
close: func() error { return nil },
}
case TypeOpenMetrics:
if strings.Contains(string(format), "version="+OpenMetricsVersion_2_0_0) {
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := MetricFamilyToOpenMetrics20(w, model.EscapeMetricFamily(v, escapingScheme), options...)
return err
},
close: func() error {
_, err := FinalizeOpenMetrics(w)
return err
},
}
}
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme), options...)
Expand Down
14 changes: 14 additions & 0 deletions expfmt/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ func TestNegotiateIncludingOpenMetrics(t *testing.T) {
acceptHeaderValue: "application/openmetrics-text;version=1.0.0",
expectedFmt: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
},
{
name: "OM format, 2.0.0 version",
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
name: "OM format, 0.0.1 version with utf-8 is not valid, falls back",
acceptHeaderValue: "application/openmetrics-text;version=0.0.1",
Expand Down Expand Up @@ -268,6 +273,15 @@ foo_metric 1.234
expOut: `# TYPE foo_metric unknown
# UNIT foo_metric seconds
foo_metric 1.234
`,
},
// 8: Untyped FmtOpenMetrics_2_0_0
{
metric: metric1,
format: FmtOpenMetrics_2_0_0,
expOut: `# TYPE foo_metric unknown
# UNIT foo_metric seconds
foo_metric 1.234
`,
},
}
Expand Down
7 changes: 7 additions & 0 deletions expfmt/expfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ const (
OpenMetricsVersion_0_0_1 = "0.0.1"
//nolint:revive // Allow for underscores.
OpenMetricsVersion_1_0_0 = "1.0.0"
//nolint:revive // Allow for underscores.
OpenMetricsVersion_2_0_0 = "2.0.0"

// The Content-Type values for the different wire protocols. Do not do direct
// comparisons to these constants, instead use the comparison functions.
Expand All @@ -59,6 +61,8 @@ const (
// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
//nolint:revive // Allow for underscores.
FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
//nolint:revive // Allow for underscores.
FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
//nolint:revive // Allow for underscores.
FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
Expand Down Expand Up @@ -114,6 +118,9 @@ func NewOpenMetricsFormat(version string) (Format, error) {
if version == OpenMetricsVersion_1_0_0 {
return FmtOpenMetrics_1_0_0, nil
}
if version == OpenMetricsVersion_2_0_0 {
return FmtOpenMetrics_2_0_0, nil
}
return FmtUnknown, errors.New("unknown open metrics version string")
}

Expand Down
291 changes: 291 additions & 0 deletions expfmt/openmetrics_2_0_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
// Copyright 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"
"errors"
"fmt"
"io"
"math"
"strconv"

dto "github.com/prometheus/client_model/go"
)

// MetricFamilyToOpenMetrics20 converts a MetricFamily proto message into the
// OpenMetrics text format version 2.0.0 and writes the resulting lines to 'out'.
// It returns the number of bytes written and any error encountered.
//
// NOTE: This method implements OpenMetrics 2.0-rc.0 which is experimental.
// Breaking changes might happen in the future. This implementation is still a
// work-in-progress, and does not yet support all features of the format.
func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) {
_ = options
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()
)

// Comments, first HELP, then TYPE.
if in.Help != nil {
n, err = w.WriteString("# HELP ")
written += n
if err != nil {
return written, err
}
n, err = writeName(w, name)
written += n
if err != nil {
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
n, err = writeEscapedString(w, *in.Help, true)
written += n
if err != nil {
return written, err
}
err = w.WriteByte('\n')
written++
if err != nil {
return written, err
}
}
n, err = w.WriteString("# TYPE ")
written += n
if err != nil {
return written, err
}
n, err = writeName(w, name)
written += n
if err != nil {
return written, err
}
switch metricType {
case dto.MetricType_COUNTER:
n, err = w.WriteString(" counter\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")
case dto.MetricType_GAUGE_HISTOGRAM:
n, err = w.WriteString(" gaugehistogram\n")
default:
// TODO: Support Info and StateSet once they are supported in the
// Prometheus protobuf format.
return written, fmt.Errorf("unknown metric type %s", metricType.String())
}
written += n
if err != nil {
return written, err
}
if in.Unit != nil {
n, err = w.WriteString("# UNIT ")
written += n
if err != nil {
return written, err
}
n, err = writeName(w, name)
written += n
if err != nil {
return written, err
}

err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
n, err = writeEscapedString(w, *in.Unit, true)
written += n
if err != nil {
return written, err
}
err = w.WriteByte('\n')
written++
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 = writeOpenMetrics20Sample(w, name, metric, metric.Counter.GetValue(), 0, false, metric.Counter.Exemplar)
case dto.MetricType_GAUGE:
if metric.Gauge == nil {
return written, fmt.Errorf("expected gauge in metric %s %s", name, metric)
}
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Gauge.GetValue(), 0, false, nil)
case dto.MetricType_UNTYPED:
if metric.Untyped == nil {
return written, fmt.Errorf("expected untyped in metric %s %s", name, metric)
}
n, err = writeOpenMetrics20Sample(w, name, metric, metric.Untyped.GetValue(), 0, false, nil)
case dto.MetricType_SUMMARY:
if metric.Summary == nil {
return written, fmt.Errorf("expected summary in metric %s %s", name, metric)
}
n, err = writeCompositeSummary(w, name, metric)
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
if metric.Histogram == nil {
return written, fmt.Errorf("expected histogram in metric %s %s", name, metric)
}
n, err = writeCompositeHistogram(w, name, metric, metricType == dto.MetricType_GAUGE_HISTOGRAM)
default:
return written, fmt.Errorf("unexpected type in metric %s %s", name, metric)
}
written += n
if err != nil {
return written, err
}
}
return written, nil
}

// writeOpenMetrics20Sample writes a single sample for simple types (Counter, Gauge, Untyped).
func writeOpenMetrics20Sample(w enhancedWriter, name string, metric *dto.Metric, floatValue float64, intValue uint64, useIntValue bool, exemplar *dto.Exemplar) (int, error) {
written := 0
n, err := writeOpenMetricsNameAndLabelPairs(w, name, metric.Label, "", 0)
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 = writeFloat(w, floatValue)
}
written += n
if err != nil {
return written, err
}

if metric.TimestampMs != nil {
err = w.WriteByte(' ')
written++
if err != nil {
return written, err
}
n, err = writeOpenMetrics20Timestamp(w, float64(*metric.TimestampMs)/1000)
written += n
if err != nil {
return written, err
}
}

// Start Timestamp for Counter
if metric.Counter != nil && metric.Counter.CreatedTimestamp != nil {
n, err = w.WriteString(" st@")
written += n
if err != nil {
return written, err
}
ts := metric.Counter.CreatedTimestamp
n, err = writeOpenMetrics20Timestamp(w, float64(ts.GetSeconds())+float64(ts.GetNanos())/1e9)
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
}

// writeOpenMetrics20Timestamp writes a float64 as a timestamp without scientific notation.
func writeOpenMetrics20Timestamp(w enhancedWriter, f float64) (int, error) {
switch {
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, 'f', -1, 64)
written, err := w.Write(*bp)
numBufPool.Put(bp)
return written, err
}
}

// Stubs for Summary and Histogram

func writeCompositeSummary(w enhancedWriter, name string, metric *dto.Metric) (int, error) {
_ = w
_ = name
_ = metric
return 0, errors.New("summary not implemented yet")
}

func writeCompositeHistogram(w enhancedWriter, name string, metric *dto.Metric, isGauge bool) (int, error) {
_ = w
_ = name
_ = metric
_ = isGauge
return 0, errors.New("histogram not implemented yet")
}
Loading
Loading