From 563548c5b9aa3ba640ab0f88b248e5554233845f Mon Sep 17 00:00:00 2001 From: dpacgdm Date: Tue, 21 Jul 2026 21:06:18 +0530 Subject: [PATCH] expfmt: add NegotiateFormatsIncludingOpenMetrics NegotiateIncludingOpenMetrics only returns the top Accept match, so servers that lack that format cannot fall back to the next recognised option without reimplementing negotiation. Return the full ordered list and keep the existing single-format helper as a thin wrapper. Signed-off-by: dpacgdm --- expfmt/encode.go | 33 ++++++++++++++++++++++++++------- expfmt/encode_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/expfmt/encode.go b/expfmt/encode.go index 73c24dfbc..d9a3545fb 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 04e94c711..fd8a2a569 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"),