Skip to content
Draft
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
45 changes: 40 additions & 5 deletions expfmt/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func init() {
FmtProtoText,
FmtProtoCompact,
FmtOpenMetrics_1_0_0,
fmtOpenMetrics_2_0_0,
FmtOpenMetrics_2_0_0,
FmtOpenMetrics_0_0_1,
} {
if parsed := goautoneg.ParseAccept(string(f)); len(parsed) > 0 {
Expand All @@ -45,6 +45,41 @@ func init() {
}
}

var (
// DefaultAcceptedFormats contains the standard accepted formats for Negotiate,
// ordered by preference (delimited protobuf, protobuf text, compact protobuf text,
// and Prometheus text format).
DefaultAcceptedFormats = []Format{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making these package vars makes them mutable as a side-effect ... are we worried about someone messing with these and changing the defaults? One possibility is to make them private and make public function accessors that return copies of the slice. It's kind of annoying that there's not a safe way to hand out immutable zero-cost copies of the default slice.

FmtProtoDelim,
FmtProtoText,
FmtProtoCompact,
FmtText,
}

// DefaultOpenMetricsAcceptedFormats contains the standard accepted formats including
// OpenMetrics 1.0.0 and 0.0.1, ordered by preference.
DefaultOpenMetricsAcceptedFormats = []Format{
FmtOpenMetrics_1_0_0,
FmtOpenMetrics_0_0_1,
FmtProtoDelim,
FmtProtoText,
FmtProtoCompact,
FmtText,
}

// DefaultOpenMetrics2AcceptedFormats contains the accepted formats including
// OpenMetrics 2.0.0, 1.0.0, and 0.0.1, ordered by preference.
DefaultOpenMetrics2AcceptedFormats = []Format{
FmtOpenMetrics_2_0_0,
FmtOpenMetrics_1_0_0,
FmtOpenMetrics_0_0_1,
FmtProtoDelim,
FmtProtoText,
FmtProtoCompact,
FmtText,
}
)

// Encoder types encode metric families into an underlying wire protocol.
type Encoder interface {
Encode(*dto.MetricFamily) error
Expand Down Expand Up @@ -78,19 +113,19 @@ func (ec encoderCloser) Close() error {
// appropriate accepted type is found, FmtText is returned (which is the
// Prometheus text format).
//
// Deprecated: Use NegotiateAccept(h, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
// Deprecated: Use NegotiateAccept(h, DefaultAcceptedFormats...)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remind me why this function is deprecated again? Having a negotiate function that uses default accepted formats seems preferable to making people get their own copy of the default formats

Would it make sense to have a private package var for the formats for Negotiate / NegotiateIncludingOpenMetrics, a getter that returns a slice for those, and to undeprecate the Negotiate / NegotiateIncludingOpenMetrics methods and just document the equivalent calls?

var defaultAcceptedFormats = ...

func DefaultAcceptedFormats() []Format {
  return slices.Clone(defaultAcceptedFormats)
}

func Negotiate(h http.Header) Format {
  return NegotiateAccept(h, defaultAcceptedFormats...)
}

// or specify only the formats supported by your server.
func Negotiate(h http.Header) Format {
return NegotiateAccept(h, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
return NegotiateAccept(h, DefaultAcceptedFormats...)
}

// NegotiateIncludingOpenMetrics works like Negotiate but includes
// FmtOpenMetrics as an option for the result.
//
// Deprecated: Use NegotiateAccept(h, FmtOpenMetrics_1_0_0, FmtOpenMetrics_0_0_1, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
// Deprecated: Use NegotiateAccept(h, DefaultOpenMetricsAcceptedFormats...)
// or specify only the formats supported by your server.
func NegotiateIncludingOpenMetrics(h http.Header) Format {
return NegotiateAccept(h, FmtOpenMetrics_1_0_0, FmtOpenMetrics_0_0_1, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
return NegotiateAccept(h, DefaultOpenMetricsAcceptedFormats...)
}

// NegotiateAccept returns the Content-Type based on the given Accept header
Expand Down
80 changes: 73 additions & 7 deletions expfmt/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func TestNegotiateAccept(t *testing.T) {
{
name: "requested OM 2.0, accepted OM 2.0",
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
acceptedFormats: []Format{fmtOpenMetrics_2_0_0, FmtText},
acceptedFormats: []Format{FmtOpenMetrics_2_0_0, FmtText},
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
Expand All @@ -265,13 +265,13 @@ func TestNegotiateAccept(t *testing.T) {
{
name: "requested OM 1.0 and 2.0, prefers higher q value",
acceptHeaderValue: "application/openmetrics-text;version=1.0.0;q=0.8, application/openmetrics-text;version=2.0.0;q=0.9",
acceptedFormats: []Format{FmtOpenMetrics_1_0_0, fmtOpenMetrics_2_0_0, FmtText},
acceptedFormats: []Format{FmtOpenMetrics_1_0_0, FmtOpenMetrics_2_0_0, FmtText},
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
name: "wildcard */* matches text format if present",
acceptHeaderValue: "*/*",
acceptedFormats: []Format{fmtOpenMetrics_2_0_0, FmtProtoDelim, FmtText},
acceptedFormats: []Format{FmtOpenMetrics_2_0_0, FmtProtoDelim, FmtText},
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
Expand All @@ -283,7 +283,7 @@ func TestNegotiateAccept(t *testing.T) {
{
name: "wildcard */* falls back to first format when no text in accepted",
acceptHeaderValue: "*/*",
acceptedFormats: []Format{fmtOpenMetrics_2_0_0, FmtProtoDelim},
acceptedFormats: []Format{FmtOpenMetrics_2_0_0, FmtProtoDelim},
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
Expand All @@ -298,6 +298,30 @@ func TestNegotiateAccept(t *testing.T) {
acceptedFormats: nil,
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
name: "DefaultOpenMetrics2AcceptedFormats with OM 2.0 accept header",
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
acceptedFormats: DefaultOpenMetrics2AcceptedFormats,
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
name: "DefaultOpenMetrics2AcceptedFormats with OM 1.0 accept header",
acceptHeaderValue: "application/openmetrics-text;version=1.0.0",
acceptedFormats: DefaultOpenMetrics2AcceptedFormats,
expectedFmt: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
},
{
name: "DefaultOpenMetrics2AcceptedFormats with proto accept header",
acceptHeaderValue: "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
acceptedFormats: DefaultOpenMetrics2AcceptedFormats,
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=values",
},
{
name: "DefaultOpenMetrics2AcceptedFormats with wildcard accept header",
acceptHeaderValue: "*/*",
acceptedFormats: DefaultOpenMetrics2AcceptedFormats,
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
}

oldDefault := model.NameEscapingScheme
Expand All @@ -318,6 +342,48 @@ func TestNegotiateAccept(t *testing.T) {
}
}

func TestDefaultAcceptedFormats(t *testing.T) {
require.Equal(t, []Format{
FmtProtoDelim,
FmtProtoText,
FmtProtoCompact,
FmtText,
}, DefaultAcceptedFormats)

require.Equal(t, []Format{
FmtOpenMetrics_1_0_0,
FmtOpenMetrics_0_0_1,
FmtProtoDelim,
FmtProtoText,
FmtProtoCompact,
FmtText,
}, DefaultOpenMetricsAcceptedFormats)

require.Equal(t, []Format{
FmtOpenMetrics_2_0_0,
FmtOpenMetrics_1_0_0,
FmtOpenMetrics_0_0_1,
FmtProtoDelim,
FmtProtoText,
FmtProtoCompact,
FmtText,
}, DefaultOpenMetrics2AcceptedFormats)

for _, accept := range []string{
"application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited",
"application/openmetrics-text;version=1.0.0",
"application/openmetrics-text;version=2.0.0",
"text/plain",
"*/*",
"unknown/format",
} {
h := http.Header{}
h.Set(hdrAccept, accept)
require.Equal(t, NegotiateAccept(h, DefaultAcceptedFormats...), Negotiate(h))
require.Equal(t, NegotiateAccept(h, DefaultOpenMetricsAcceptedFormats...), NegotiateIncludingOpenMetrics(h))
}
}

func TestEncode(t *testing.T) {
metric1 := &dto.MetricFamily{
Name: proto.String("foo_metric"),
Expand Down Expand Up @@ -388,10 +454,10 @@ foo_metric 1.234
foo_metric 1.234
`,
},
// 8: Untyped fmtOpenMetrics_2_0_0
// 8: Untyped FmtOpenMetrics_2_0_0
{
metric: metric1,
format: fmtOpenMetrics_2_0_0,
format: FmtOpenMetrics_2_0_0,
expOut: `# TYPE foo_metric unknown
# UNIT foo_metric seconds
foo_metric 1.234
Expand Down Expand Up @@ -651,7 +717,7 @@ func TestNewEncoder_OpenMetricsVersionDispatch(t *testing.T) {
},
{
name: "OpenMetrics 2.0.0 standard",
format: fmtOpenMetrics_2_0_0,
format: FmtOpenMetrics_2_0_0,
expectedLine: "# TYPE test_counter counter\ntest_counter 42.0 st@1234567890\n",
},
{
Expand Down
78 changes: 55 additions & 23 deletions expfmt/expfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package expfmt

import (
"errors"
"mime"
"strings"

"github.com/prometheus/common/model"
Expand All @@ -25,18 +26,13 @@ import (
type Format string

// Constants to assemble the Content-Type values for the different wire
// protocols. The Content-Type strings here are all for the legacy exposition
// formats, where valid characters for metric names and label names are limited.
// Support for arbitrary UTF-8 characters in those names is already partially
// implemented in this module (see model.ValidationScheme), but to actually use
// it on the wire, new content-type strings will have to be agreed upon and
// added here.
// protocols.
const (
TextVersion = "0.0.4"
ProtoType = `application/vnd.google.protobuf`
ProtoProtocol = `io.prometheus.client.MetricFamily`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead.
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"

OpenMetricsType = `application/openmetrics-text`
//nolint:revive // Allow for underscores.
OpenMetricsVersion_0_0_1 = "0.0.1"
Expand All @@ -45,25 +41,23 @@ const (
//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.
//
// Deprecated: Use expfmt.NewFormat(expfmt.TypeUnknown) instead.
// The Content-Type values for the different wire protocols. These represent
// baseline Content-Types used for HTTP headers and content negotiation.
// Because Content-Types on the wire may include dynamic parameters
// (such as "; escaping=...") or whitespace variations, avoid comparing Format
// values using direct equality (==). Instead, use the Matches method, e.g.
// format.Matches(expfmt.FmtText).
FmtUnknown Format = `<unknown>`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeTextPlain) instead.
FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoDelim) instead.
FmtProtoDelim Format = ProtoFmt + ` encoding=delimited`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoText) instead.
FmtProtoText Format = ProtoFmt + ` encoding=text`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead.
FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8`

FmtProtoDelim Format = ProtoFmt + ` encoding=delimited`
FmtProtoText Format = ProtoFmt + ` encoding=text`
FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text`
// 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.
FmtOpenMetrics_2_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_2_0_0 + `; charset=utf-8`
//nolint:revive // Allow for underscores.
FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
)
Expand Down Expand Up @@ -123,11 +117,49 @@ func NewOpenMetricsFormat(version string) (Format, error) {
}
if version == OpenMetricsVersion_2_0_0 {
// OpenMetrics 2.0.0 is experimental and encode-only.
return fmtOpenMetrics_2_0_0, nil
return FmtOpenMetrics_2_0_0, nil
}
return FmtUnknown, errors.New("unknown open metrics version string")
}

// Version returns the format version parameter (e.g. "0.0.4", "1.0.0", "2.0.0"),
// or an empty string if unspecified or invalid.
func (f Format) Version() string {
_, params, err := mime.ParseMediaType(string(f))
if err != nil {
return ""
}
return params["version"]
}

// Matches reports whether format f matches the target Format.
// The comparison matches the media type and essential parameters (such as
// version, proto, and encoding) while ignoring dynamic parameters like
// escaping scheme ("; escaping=...") or formatting/whitespace differences.
func (f Format) Matches(target Format) bool {
if f == target {
return true
}
mediaType1, params1, err1 := mime.ParseMediaType(string(f))
mediaType2, params2, err2 := mime.ParseMediaType(string(target))
if err1 != nil || err2 != nil {
return f == target
}
if mediaType1 != mediaType2 {
return false
}
if params1["version"] != params2["version"] {
return false
}
if params1["encoding"] != params2["encoding"] {
return false
}
if params1["proto"] != params2["proto"] {
return false
}
return true
}

// WithEscapingScheme returns a copy of Format with the specified escaping
// scheme appended to the end. If an escaping scheme already exists it is
// removed.
Expand Down
Loading
Loading