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
59 changes: 52 additions & 7 deletions expfmt/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,32 @@ func ResponseFormat(h http.Header) Format {
return FmtUnknown
}
return FmtText
case OpenMetricsType:
if c, ok := params["charset"]; ok && c != "utf-8" {
return FmtUnknown
}
switch params["version"] {
case "", OpenMetricsVersion_0_0_1:
return FmtOpenMetrics_0_0_1
case OpenMetricsVersion_1_0_0:
return FmtOpenMetrics_1_0_0
default:
return FmtUnknown
}
}

return FmtUnknown
}

// 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
Comment thread
martincostello marked this conversation as resolved.
// 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.
// See: https://github.com/prometheus/common/issues/812
// and the Prometheus/OpenMetrics text formats. For historical reasons, this
// decoder fallbacks to classic text decoding for any other format. This decoder
// may not support the latest features of the text formats 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
if format.ToEscapingScheme() == model.NoEscaping {
Expand All @@ -88,6 +99,8 @@ func NewDecoder(r io.Reader, format Format) Decoder {
switch format.FormatType() {
case TypeProtoDelim:
return &protoDecoder{r: bufio.NewReader(r), s: scheme}
case TypeOpenMetrics:
return &openMetricsDecoder{r: r, s: scheme}
case TypeProtoText, TypeProtoCompact:
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
}
Expand Down Expand Up @@ -139,6 +152,38 @@ func (d *errDecoder) Decode(*dto.MetricFamily) error {
return d.err
}

// openMetricsDecoder implements the Decoder interface for the OpenMetrics text protocol.
type openMetricsDecoder struct {
r io.Reader
fams map[string]*dto.MetricFamily
s model.ValidationScheme
err error
}

// Decode implements the Decoder interface.
func (d *openMetricsDecoder) Decode(mf *dto.MetricFamily) error {
if d.err == nil {
// Read all metrics in one shot.
p := OpenMetricsParser{scheme: d.s}
d.fams, d.err = p.OpenMetricsToMetricFamilies(d.r)
// If we don't get an error, store io.EOF for the end.
if d.err == nil {
d.err = io.EOF
}
}
// Pick off one MetricFamily per Decode until there's nothing left.
for key, fam := range d.fams {
mf.Name = fam.Name
mf.Help = fam.Help
mf.Type = fam.Type
mf.Unit = fam.Unit
mf.Metric = fam.Metric
delete(d.fams, key)
return nil
}
return d.err
}

// textDecoder implements the Decoder interface for the text protocol.
type textDecoder struct {
r io.Reader
Expand Down
122 changes: 122 additions & 0 deletions expfmt/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,112 @@ mf2 4
require.Truef(t, reflect.DeepEqual(all, out), "output does not match")
}

func TestOpenMetricsDecoder(t *testing.T) {
var (
ts = model.Now()
in = `
# Only a quite simple scenario with two metric families.
# More complicated tests of the parser itself can be found in the OpenMetrics parser tests.
# TYPE metric1 counter
metric1_total 3
mf1{label="value1"} -3.14 123456
mf1{label="value2"} 42
metric1_total 4
# EOF
`
out = model.Vector{
&model.Sample{
Metric: model.Metric{
model.MetricNameLabel: "mf1",
"label": "value1",
},
Value: -3.14,
Timestamp: 123456,
},
&model.Sample{
Metric: model.Metric{
model.MetricNameLabel: "mf1",
"label": "value2",
},
Value: 42,
Timestamp: ts,
},
&model.Sample{
Metric: model.Metric{
model.MetricNameLabel: "metric1",
},
Value: 3,
Timestamp: ts,
},
&model.Sample{
Metric: model.Metric{
model.MetricNameLabel: "metric1",
},
Value: 4,
Timestamp: ts,
},
}
)

dec := &SampleDecoder{
Dec: NewDecoder(strings.NewReader(in), FmtOpenMetrics_1_0_0),
Opts: &DecodeOptions{
Timestamp: ts,
},
}
var all model.Vector
for {
var smpls model.Vector
err := dec.Decode(&smpls)
if err != nil && errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
all = append(all, smpls...)
}
sort.Sort(all)
sort.Sort(out)
require.Truef(t, reflect.DeepEqual(all, out), "output does not match")
}

func TestOpenMetricsDecoderWithUTF8Names(t *testing.T) {
dec := NewDecoder(
strings.NewReader(`# TYPE "métric" gauge
{"métric","labél"="value"} 1
# EOF
`),
FmtOpenMetrics_1_0_0.WithEscapingScheme(model.NoEscaping),
)

var mf dto.MetricFamily
require.NoError(t, dec.Decode(&mf))
require.Equal(t, "métric", mf.GetName())
require.Len(t, mf.GetMetric(), 1)
require.Len(t, mf.GetMetric()[0].GetLabel(), 1)
require.Equal(t, "labél", mf.GetMetric()[0].GetLabel()[0].GetName())
}

func TestOpenMetricsDecoderInfoFamilyUsesBaseName(t *testing.T) {
dec := NewDecoder(
strings.NewReader(`# TYPE target info
# HELP target help
target_info{service_name="service"} 1
# EOF
`),
FmtOpenMetrics_1_0_0,
)

var mf dto.MetricFamily
require.NoError(t, dec.Decode(&mf))
require.Equal(t, "target", mf.GetName())
require.Equal(t, "help", mf.GetHelp())
require.Equal(t, dto.MetricType_UNTYPED, mf.GetType())
require.Len(t, mf.GetMetric(), 1)
require.Len(t, mf.GetMetric()[0].GetLabel(), 1)
require.Equal(t, "service_name", mf.GetMetric()[0].GetLabel()[0].GetName())
require.Equal(t, "service", mf.GetMetric()[0].GetLabel()[0].GetValue())
}

func TestProtoDecoder(t *testing.T) {
testTime := model.Now()

Expand Down Expand Up @@ -454,6 +560,22 @@ func testDiscriminatorHTTPHeader(t testing.TB) {
input: map[string]string{"Content-Type": `text/plain; version=0.0.3`},
output: FmtUnknown,
},
{
input: map[string]string{"Content-Type": `application/openmetrics-text; version=1.0.0; charset=utf-8`},
output: FmtOpenMetrics_1_0_0,
},
{
input: map[string]string{"Content-Type": `application/openmetrics-text; version=0.0.1; charset=utf-8`},
output: FmtOpenMetrics_0_0_1,
},
{
input: map[string]string{"Content-Type": `application/openmetrics-text; charset=utf-8`},
output: FmtOpenMetrics_0_0_1,
},
{
input: map[string]string{"Content-Type": `application/openmetrics-text; version=1.0.0; charset=latin-1`},
output: FmtUnknown,
},
}

for i, scenario := range scenarios {
Expand Down
Loading
Loading