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
19 changes: 13 additions & 6 deletions expfmt/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ func ResponseFormat(h http.Header) Format {

// NewDecoder returns a new decoder based on the given input format. Metric
// names are validated based on the provided Format -- if the format requires
// escaping, raditional Prometheues validity checking is used. Otherwise, names
// escaping, traditional Prometheus validity checking is used. Otherwise, names
// are checked for UTF-8 validity. Supported formats include delimited protobuf
// and Prometheus text format. For historical reasons, this decoder fallbacks
// to classic text decoding for any other format. This decoder does not fully
// support OpenMetrics although it may often succeed due to the similarities
// between the formats. This decoder may not support the latest features of
// Prometheus text format and is not intended for high-performance applications.
// and Prometheus text format. For historical reasons, this decoder falls back
// to classic text decoding for other legacy formats, but returns an error for
// unsupported formats such as OpenMetrics 2.0. This decoder does not fully
// support OpenMetrics although it may often succeed for OpenMetrics 1.0 due to
// the similarities between the formats. This decoder may not support the latest
// features of Prometheus text format and is not intended for high-performance
// applications.
// See: https://github.com/prometheus/common/issues/812
func NewDecoder(r io.Reader, format Format) Decoder {
scheme := model.LegacyValidation
Expand All @@ -90,6 +92,11 @@ func NewDecoder(r io.Reader, format Format) Decoder {
return &protoDecoder{r: bufio.NewReader(r), s: scheme}
case TypeProtoText, TypeProtoCompact:
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
case TypeOpenMetrics:
_, params, err := mime.ParseMediaType(string(format))
if err == nil && params["version"] == OpenMetricsVersion_2_0_0 {
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
}
Comment thread
dashpole marked this conversation as resolved.
}
return &textDecoder{r: r, s: scheme}
}
Expand Down
67 changes: 67 additions & 0 deletions expfmt/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"math"
"net/http"
Expand Down Expand Up @@ -577,3 +578,69 @@ func TestTextDecoderWithBufioReader(t *testing.T) {
}
require.Truef(t, decoded, "Metric foo not decoded")
}

func TestNewDecoder(t *testing.T) {
om20Format, err := NewOpenMetricsFormat(OpenMetricsVersion_2_0_0)
require.NoError(t, err)

tests := []struct {
name string
format Format
expectError bool
}{
{
name: "Text format",
format: FmtText,
expectError: false,
},
{
name: "ProtoDelim format",
format: FmtProtoDelim,
expectError: false,
},
{
name: "ProtoText format",
format: FmtProtoText,
expectError: true,
},
{
name: "ProtoCompact format",
format: FmtProtoCompact,
expectError: true,
},
{
name: "OpenMetrics 0.0.1",
format: FmtOpenMetrics_0_0_1,
expectError: false,
},
{
name: "OpenMetrics 1.0.0",
format: FmtOpenMetrics_1_0_0,
expectError: false,
},
{
name: "OpenMetrics 2.0.0",
format: om20Format,
expectError: true,
},
{
name: "OpenMetrics 2.0.0 with escaping",
format: om20Format.WithEscapingScheme(model.ValueEncodingEscaping),
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dec := NewDecoder(strings.NewReader(""), tt.format)
var mf dto.MetricFamily
err := dec.Decode(&mf)
if tt.expectError {
require.Error(t, err)
require.Contains(t, err.Error(), fmt.Sprintf("format %s not supported for decoding", tt.format))
} else if err != nil {
require.ErrorIs(t, err, io.EOF)
}
})
}
}
5 changes: 3 additions & 2 deletions expfmt/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ package expfmt
import (
"fmt"
"io"
"mime"
"net/http"
"strings"

"github.com/munnerz/goautoneg"
dto "github.com/prometheus/client_model/go"
Expand Down Expand Up @@ -222,7 +222,8 @@ 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) {
_, params, err := mime.ParseMediaType(string(format))
if err == nil && params["version"] == OpenMetricsVersion_2_0_0 {
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := MetricFamilyToOpenMetrics20(w, model.EscapeMetricFamily(v, escapingScheme), options...)
Expand Down
74 changes: 74 additions & 0 deletions expfmt/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ package expfmt
import (
"bytes"
"net/http"
"strings"
"testing"

dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/prometheus/common/model"
)
Expand Down Expand Up @@ -614,3 +616,75 @@ func BenchmarkNegotiateAccept(b *testing.B) {
_ = NegotiateAccept(h, accepted...)
}
}

func TestNewEncoder_OpenMetricsVersionDispatch(t *testing.T) {
counterMetric := &dto.MetricFamily{
Name: proto.String("test_counter"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(42),
CreatedTimestamp: &timestamppb.Timestamp{
Seconds: 1234567890,
Nanos: 0,
},
},
},
},
}

tests := []struct {
name string
format Format
expectedLine string
}{
{
name: "OpenMetrics 1.0.0",
format: FmtOpenMetrics_1_0_0,
expectedLine: "# TYPE test_counter unknown\ntest_counter 42.0\n",
},
{
name: "OpenMetrics 0.0.1",
format: FmtOpenMetrics_0_0_1,
expectedLine: "# TYPE test_counter unknown\ntest_counter 42.0\n",
},
{
name: "OpenMetrics 2.0.0 standard",
format: fmtOpenMetrics_2_0_0,
expectedLine: "# TYPE test_counter counter\ntest_counter 42.0 st@1234567890\n",
},
{
name: "OpenMetrics 2.0.0 reordered parameters",
format: Format("application/openmetrics-text; charset=utf-8; version=2.0.0"),
expectedLine: "# TYPE test_counter counter\ntest_counter 42.0 st@1234567890\n",
},
{
name: "OpenMetrics 2.0.0 quoted version parameter",
format: Format(`application/openmetrics-text; version="2.0.0"; charset=utf-8`),
expectedLine: "# TYPE test_counter counter\ntest_counter 42.0 st@1234567890\n",
},
{
name: "OpenMetrics 2.0.0 with escaping scheme",
format: Format("application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values"),
expectedLine: "# TYPE test_counter counter\ntest_counter 42.0 st@1234567890\n",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
enc := NewEncoder(&buf, tt.format)
err := enc.Encode(counterMetric)
require.NoError(t, err)
closer, ok := enc.(Closer)
require.True(t, ok)
err = closer.Close()
require.NoError(t, err)

output := buf.String()
require.Contains(t, output, tt.expectedLine)
require.True(t, strings.HasSuffix(output, "# EOF\n"))
})
}
}
4 changes: 4 additions & 0 deletions expfmt/expfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ func NewFormat(t FormatType) Format {

// NewOpenMetricsFormat generates a new OpenMetrics format matching the
// specified version number.
//
// Note: OpenMetrics version 2.0.0 is experimental and encode-only (currently
// supporting counter, gauge, and untyped metric types).
func NewOpenMetricsFormat(version string) (Format, error) {
if version == OpenMetricsVersion_0_0_1 {
return FmtOpenMetrics_0_0_1, nil
Expand All @@ -119,6 +122,7 @@ func NewOpenMetricsFormat(version string) (Format, error) {
return FmtOpenMetrics_1_0_0, nil
}
if version == OpenMetricsVersion_2_0_0 {
// OpenMetrics 2.0.0 is experimental and encode-only (counter/gauge/untyped).
return fmtOpenMetrics_2_0_0, nil
}
return FmtUnknown, errors.New("unknown open metrics version string")
Expand Down
6 changes: 5 additions & 1 deletion expfmt/openmetrics_2_0_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ import (
// 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.
// NOTE: This method targets OpenMetrics 2.0.0 (currently aligned with 2.0-rc.0) which is experimental and
// encode-only (currently supporting counter, gauge, and untyped metric types).
// 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.
// EncoderOptions are accepted for signature compatibility with
// MetricFamilyToOpenMetrics and are currently ignored.
func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options ...EncoderOption) (written int, err error) {
// Options are accepted for signature compatibility and ignored.
_ = options
name := in.GetName()
if name == "" {
Expand Down
Loading