diff --git a/expfmt/encode.go b/expfmt/encode.go index 73c24dfb..d9a3545f 100644 --- a/expfmt/encode.go +++ b/expfmt/encode.go @@ -94,7 +94,21 @@ func Negotiate(h http.Header) Format { // 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 { + formats := NegotiateFormatsIncludingOpenMetrics(h) + return formats[0] +} + +// NegotiateFormatsIncludingOpenMetrics works like NegotiateIncludingOpenMetrics +// but returns all recognised formats from the Accept header in descending +// preference order instead of stopping at the first match. If no recognised +// formats are found, the slice contains FmtText (with the negotiated escaping +// scheme) as the default fallback. +// +// Callers that only support a subset of formats can iterate the result and pick +// the first format they can serve. +func NegotiateFormatsIncludingOpenMetrics(h http.Header) []Format { escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) + var formats []Format for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { switch Format(escapeParam) { @@ -108,26 +122,31 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format { if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { switch ac.Params["encoding"] { case "delimited": - return FmtProtoDelim + escapingScheme + formats = append(formats, FmtProtoDelim+escapingScheme) case "text": - return FmtProtoText + escapingScheme + formats = append(formats, FmtProtoText+escapingScheme) case "compact-text": - return FmtProtoCompact + escapingScheme + formats = append(formats, FmtProtoCompact+escapingScheme) } + continue } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { - return FmtText + escapingScheme + formats = append(formats, FmtText+escapingScheme) + continue } 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 + formats = append(formats, FmtOpenMetrics_1_0_0+escapingScheme) default: - return FmtOpenMetrics_0_0_1 + escapingScheme + formats = append(formats, FmtOpenMetrics_0_0_1+escapingScheme) } } } - return FmtText + escapingScheme + if len(formats) == 0 { + return []Format{FmtText + escapingScheme} + } + return formats } // NewEncoder returns a new encoder based on content type negotiation. All diff --git a/expfmt/encode_test.go b/expfmt/encode_test.go index 04e94c71..fd8a2a56 100644 --- a/expfmt/encode_test.go +++ b/expfmt/encode_test.go @@ -200,6 +200,36 @@ func TestNegotiateIncludingOpenMetrics(t *testing.T) { } } +func TestNegotiateFormatsIncludingOpenMetrics(t *testing.T) { + oldDefault := model.NameEscapingScheme + model.NameEscapingScheme = model.ValueEncodingEscaping + defer func() { + model.NameEscapingScheme = oldDefault + }() + + h := http.Header{} + h.Add(hdrAccept, "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.6,application/openmetrics-text;version=1.0.0;escaping=allow-utf-8;q=0.5,text/plain;version=0.0.4;q=0.2") + + got := NegotiateFormatsIncludingOpenMetrics(h) + want := []Format{ + FmtProtoDelim + "; escaping=values", + FmtOpenMetrics_1_0_0 + "; escaping=allow-utf-8", + FmtText + "; escaping=allow-utf-8", + } + if len(got) != len(want) { + t.Fatalf("expected %d formats, got %d: %#v", len(want), len(got), got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("format[%d]: want %q, got %q", i, want[i], got[i]) + } + } + // First entry must match NegotiateIncludingOpenMetrics. + if NegotiateIncludingOpenMetrics(h) != got[0] { + t.Errorf("NegotiateIncludingOpenMetrics should return first negotiated format") + } +} + func TestEncode(t *testing.T) { metric1 := &dto.MetricFamily{ Name: proto.String("foo_metric"),