Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
14 changes: 14 additions & 0 deletions expfmt/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,19 @@ func BenchmarkConvertMetricFamily(b *testing.B) {
out.Reset()
}
})
b.Run("OM2.0/"+mf.GetType().String(), func(b *testing.B) {
out := bytes.NewBuffer(make([]byte, 0, 1024))
if _, err := MetricFamilyToOpenMetrics20(out, mf); err != nil {
b.Skipf("skipping unsupported type: %v", err)
}
out.Reset()
for b.Loop() {
_, err := MetricFamilyToOpenMetrics20(out, mf)
if err != nil {
b.Fatal(err)
}
out.Reset()
}
})
}
}
135 changes: 86 additions & 49 deletions 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 @@ -57,10 +58,29 @@ func (ec encoderCloser) Close() error {

// Negotiate returns the Content-Type based on the given Accept header. If no
// appropriate accepted type is found, FmtText is returned (which is the
// Prometheus text format). This function will never negotiate FmtOpenMetrics,
// as the support is still experimental. To include the option to negotiate
// FmtOpenMetrics, use NegotiateIncludingOpenMetrics.
// Prometheus text format).
//
// Deprecated: Use NegotiateAccept(h, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
// or specify only the formats supported by your server.
func Negotiate(h http.Header) Format {
return NegotiateAccept(h, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
}

// 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)
// or specify only the formats supported by your server.
func NegotiateIncludingOpenMetrics(h http.Header) Format {
Comment thread
dashpole marked this conversation as resolved.
return NegotiateAccept(h, FmtOpenMetrics_1_0_0, FmtOpenMetrics_0_0_1, FmtProtoDelim, FmtProtoText, FmtProtoCompact, FmtText)
}

// NegotiateAccept returns the Content-Type based on the given Accept header
// and the list of accepted Formats provided by the caller, in order of preference.
// If no accepted format matches the Accept header, it falls back to the text
// format if present in the accepted list, or the first accepted format (or FmtText
// if accepted is empty).
func NegotiateAccept(h http.Header, accepted ...Format) Format {
escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String())))
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
Expand All @@ -71,63 +91,68 @@ func Negotiate(h http.Header) Format {
// If the escaping parameter is unknown, ignore it.
}
}
ver := ac.Params["version"]
if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
switch ac.Params["encoding"] {
case "delimited":
return FmtProtoDelim + escapingScheme
case "text":
return FmtProtoText + escapingScheme
case "compact-text":
return FmtProtoCompact + escapingScheme

for _, f := range accepted {
if matchFormat(ac, f) {
return f + escapingScheme
}
}
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
return FmtText + escapingScheme
}
for _, f := range accepted {
if f.FormatType() == TypeTextPlain {
return f + escapingScheme
}
}
if len(accepted) > 0 {
return accepted[0] + escapingScheme
}
return FmtText + escapingScheme
}

// NegotiateIncludingOpenMetrics works like Negotiate but includes
// FmtOpenMetrics as an option for the result. Note that this function is
// temporary and will disappear once FmtOpenMetrics is fully supported and as
// such may be negotiated by the normal Negotiate function.
func NegotiateIncludingOpenMetrics(h http.Header) Format {
escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String())))
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" {
switch Format(escapeParam) {
case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues:
escapingScheme = Format("; escaping=" + escapeParam)
default:
// If the escaping parameter is unknown, ignore it.
}
}
ver := ac.Params["version"]
if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol {
switch ac.Params["encoding"] {
case "delimited":
return FmtProtoDelim + escapingScheme
case "text":
return FmtProtoText + escapingScheme
case "compact-text":
return FmtProtoCompact + escapingScheme
}
// matchFormat checks if a parsed accept clause matches a given Format.
func matchFormat(ac goautoneg.Accept, f Format) bool {
parsed := goautoneg.ParseAccept(string(f))
if len(parsed) == 0 {
return false
}
target := parsed[0]

if ac.Type != "*" && ac.Type != target.Type {
return false
}
if ac.SubType != "*" && ac.SubType != target.SubType {
return false
}

// If ac is */*, wildcard matches any target.
if ac.Type == "*" && ac.SubType == "*" {
return true
}

// Default OpenMetrics version to OpenMetricsVersion_0_0_1.
acVersion := ac.Params["version"]
if acVersion == "" && ac.Type+"/"+ac.SubType == OpenMetricsType {
acVersion = OpenMetricsVersion_0_0_1
}
if acVersion == "" && ac.Type == "text" && ac.SubType == "plain" {
acVersion = TextVersion
}

// General param matching.
for k, v := range target.Params {
if k == "charset" {
continue
}
if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") {
return FmtText + escapingScheme
acVal := ac.Params[k]
if k == "version" {
acVal = acVersion
}
if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") {
switch ver {
case OpenMetricsVersion_1_0_0:
return FmtOpenMetrics_1_0_0 + escapingScheme
default:
return FmtOpenMetrics_0_0_1 + escapingScheme
}
if acVal != v {
return false
}
}
return FmtText + escapingScheme

return true
}

// NewEncoder returns a new encoder based on content type negotiation. All
Expand Down Expand Up @@ -181,6 +206,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
89 changes: 89 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: "text/plain; version=0.0.4; 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 @@ -200,6 +205,81 @@ func TestNegotiateIncludingOpenMetrics(t *testing.T) {
}
}

func TestNegotiateAccept(t *testing.T) {
tests := []struct {
name string
acceptHeaderValue string
acceptedFormats []Format
expectedFmt string
}{
{
name: "requested OM 2.0, accepted OM 2.0",
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
acceptedFormats: []Format{fmtOpenMetrics_2_0_0, FmtText},
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
name: "requested OM 2.0, not accepted, falls back to text",
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
acceptedFormats: []Format{FmtOpenMetrics_1_0_0, FmtText},
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
name: "requested OM 2.0, not accepted, falls back to first format when no text in accepted",
acceptHeaderValue: "application/openmetrics-text;version=2.0.0",
acceptedFormats: []Format{FmtProtoDelim},
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=values",
},
{
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},
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
name: "wildcard */* matches first accepted format",
acceptHeaderValue: "*/*",
acceptedFormats: []Format{fmtOpenMetrics_2_0_0, FmtProtoDelim, FmtText},
expectedFmt: "application/openmetrics-text; version=2.0.0; charset=utf-8; escaping=values",
},
{
name: "wildcard */* with text first in accepted",
acceptHeaderValue: "*/*",
acceptedFormats: []Format{FmtText, FmtProtoDelim},
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
name: "unversioned text/plain matches FmtText",
acceptHeaderValue: "text/plain",
acceptedFormats: []Format{FmtProtoDelim, FmtText},
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
name: "empty accepted list defaults to FmtText",
acceptHeaderValue: "application/unknown",
acceptedFormats: nil,
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
}

oldDefault := model.NameEscapingScheme
model.NameEscapingScheme = model.ValueEncodingEscaping
defer func() {
model.NameEscapingScheme = oldDefault
}()

for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
h := http.Header{}
h.Add(hdrAccept, test.acceptHeaderValue)
actualFmt := string(NegotiateAccept(h, test.acceptedFormats...))
if actualFmt != test.expectedFmt {
t.Errorf("case %d: expected NegotiateAccept to return format %s, but got %s instead", i, test.expectedFmt, actualFmt)
}
})
}
}

func TestEncode(t *testing.T) {
metric1 := &dto.MetricFamily{
Name: proto.String("foo_metric"),
Expand Down Expand Up @@ -268,6 +348,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
Loading
Loading