blob: 04e94c711a9a0b6a1f6f1465e2c05869843752c4 [file] [edit]
// Copyright 2018 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package expfmt
import (
"bytes"
"net/http"
"testing"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"github.com/prometheus/common/model"
)
func TestNegotiate(t *testing.T) {
acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily"
tests := []struct {
name string
acceptHeaderValue string
expectedFmt string
}{
{
name: "delimited format",
acceptHeaderValue: acceptValuePrefix + ";encoding=delimited",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
},
{
name: "text format",
acceptHeaderValue: acceptValuePrefix + ";encoding=text",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
},
{
name: "compact text format",
acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
},
{
name: "plain text format",
acceptHeaderValue: "text/plain;version=0.0.4",
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
{
name: "delimited format utf-8",
acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8",
},
{
name: "text format utf-8",
acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8",
},
{
name: "compact text format utf-8",
acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8",
},
{
name: "plain text format 0.0.4 with utf-8 not valid, falls back",
acceptHeaderValue: "text/plain;version=0.0.4;",
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=underscores",
},
{
name: "plain text format 0.0.4 with utf-8 not valid, falls back",
acceptHeaderValue: "text/plain;version=0.0.4; escaping=values;",
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
}
oldDefault := model.NameEscapingScheme
model.NameEscapingScheme = model.UnderscoreEscaping
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(Negotiate(h))
if actualFmt != test.expectedFmt {
t.Errorf("case %d: expected Negotiate to return format %s, but got %s instead", i, test.expectedFmt, actualFmt)
}
})
}
}
func TestNegotiateIncludingOpenMetrics(t *testing.T) {
acceptValuePrefix := "application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily"
tests := []struct {
name string
acceptHeaderValue string
expectedFmt string
}{
{
name: "OM format, no version",
acceptHeaderValue: "application/openmetrics-text",
expectedFmt: "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values",
},
{
name: "OM format, 0.0.1 version",
acceptHeaderValue: "application/openmetrics-text;version=0.0.1; escaping=underscores",
expectedFmt: "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=underscores",
},
{
name: "OM format, 1.0.0 version",
acceptHeaderValue: "application/openmetrics-text;version=1.0.0",
expectedFmt: "application/openmetrics-text; version=1.0.0; 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",
expectedFmt: "application/openmetrics-text; version=0.0.1; charset=utf-8; escaping=values",
},
{
name: "OM format, 1.0.0 version with utf-8 is not valid, falls back",
acceptHeaderValue: "application/openmetrics-text;version=1.0.0; escaping=values;",
expectedFmt: "application/openmetrics-text; version=1.0.0; charset=utf-8; escaping=values",
},
{
name: "OM format, invalid version",
acceptHeaderValue: "application/openmetrics-text;version=0.0.4",
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
name: "compact text format",
acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
},
{
name: "plain text format",
acceptHeaderValue: "text/plain;version=0.0.4",
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=values",
},
{
name: "plain text format 0.0.4",
acceptHeaderValue: "text/plain;version=0.0.4; escaping=allow-utf-8",
expectedFmt: "text/plain; version=0.0.4; charset=utf-8; escaping=allow-utf-8",
},
{
name: "delimited format utf-8",
acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=allow-utf-8;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=allow-utf-8",
},
{
name: "text format utf-8",
acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=allow-utf-8;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=allow-utf-8",
},
{
name: "compact text format utf-8",
acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=allow-utf-8;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=allow-utf-8",
},
{
name: "delimited format escaped",
acceptHeaderValue: acceptValuePrefix + ";encoding=delimited; escaping=underscores;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited; escaping=underscores",
},
{
name: "text format escaped",
acceptHeaderValue: acceptValuePrefix + ";encoding=text; escaping=underscores;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=text; escaping=underscores",
},
{
name: "compact text format escaped",
acceptHeaderValue: acceptValuePrefix + ";encoding=compact-text; escaping=underscores;",
expectedFmt: "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=compact-text; escaping=underscores",
},
}
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(NegotiateIncludingOpenMetrics(h))
if actualFmt != test.expectedFmt {
t.Errorf("case %d: expected Negotiate 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"),
Type: dto.MetricType_UNTYPED.Enum(),
Unit: proto.String("seconds"),
Metric: []*dto.Metric{
{
Untyped: &dto.Untyped{
Value: proto.Float64(1.234),
},
},
},
}
scenarios := []struct {
metric *dto.MetricFamily
format Format
options []EncoderOption
expOut string
}{
// 1: Untyped ProtoDelim
{
metric: metric1,
format: FmtProtoDelim,
},
// 2: Untyped FmtProtoCompact
{
metric: metric1,
format: FmtProtoCompact,
},
// 3: Untyped FmtProtoText
{
metric: metric1,
format: FmtProtoText,
},
// 4: Untyped FmtText
{
metric: metric1,
format: FmtText,
expOut: `# TYPE foo_metric untyped
foo_metric 1.234
`,
},
// 5: Untyped FmtOpenMetrics_0_0_1
{
metric: metric1,
format: FmtOpenMetrics_0_0_1,
expOut: `# TYPE foo_metric unknown
# UNIT foo_metric seconds
foo_metric 1.234
`,
},
// 6: Untyped FmtOpenMetrics_1_0_0
{
metric: metric1,
format: FmtOpenMetrics_1_0_0,
expOut: `# TYPE foo_metric unknown
# UNIT foo_metric seconds
foo_metric 1.234
`,
},
// 7: Simple Counter FmtOpenMetrics_1_0_0
{
metric: metric1,
format: FmtOpenMetrics_1_0_0,
expOut: `# TYPE foo_metric unknown
# UNIT foo_metric seconds
foo_metric 1.234
`,
},
}
for i, scenario := range scenarios {
out := bytes.NewBuffer(make([]byte, 0, len(scenario.expOut)))
enc := NewEncoder(out, scenario.format, scenario.options...)
err := enc.Encode(scenario.metric)
if err != nil {
t.Errorf("%d. error: %s", i, err)
continue
}
if expected, got := len(scenario.expOut), len(out.Bytes()); expected != 0 && expected != got {
t.Errorf(
"%d. expected %d bytes written, got %d",
i, expected, got,
)
}
if expected, got := scenario.expOut, out.String(); expected != "" && expected != got {
t.Errorf(
"%d. expected out=%q, got %q",
i, expected, got,
)
}
if len(out.Bytes()) == 0 {
t.Errorf(
"%d. expected output not to be empty",
i,
)
}
}
}
func TestEscapedEncode(t *testing.T) {
tests := []struct {
name string
format Format
}{
{
name: "ProtoDelim",
format: FmtProtoDelim,
},
{
name: "ProtoDelim with escaping underscores",
format: FmtProtoDelim + "; escaping=underscores",
},
{
name: "ProtoCompact",
format: FmtProtoCompact,
},
{
name: "ProtoText",
format: FmtProtoText,
},
{
name: "Text",
format: FmtText,
},
}
metric := &dto.MetricFamily{
Name: proto.String("foo.metric"),
Type: dto.MetricType_UNTYPED.Enum(),
Metric: []*dto.Metric{
{
Untyped: &dto.Untyped{
Value: proto.Float64(1.234),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("dotted.label.name"),
Value: proto.String("my.label.value"),
},
},
Untyped: &dto.Untyped{
Value: proto.Float64(8),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buff bytes.Buffer
encoder := NewEncoder(&buff, tt.format)
err := encoder.Encode(metric)
require.NoError(t, err)
s := buff.String()
assert.NotContains(t, s, "foo.metric")
assert.Contains(t, s, "foo_metric")
assert.NotContains(t, s, "dotted.label.name")
assert.Contains(t, s, "dotted_label_name")
assert.Contains(t, s, "my.label.value")
})
}
}
func TestDottedEncode(t *testing.T) {
//nolint:staticcheck
model.NameValidationScheme = model.UTF8Validation
metric := &dto.MetricFamily{
Name: proto.String("foo.metric"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(1.234),
},
},
{
Label: []*dto.LabelPair{
{
Name: proto.String("dotted.label.name"),
Value: proto.String("my.label.value"),
},
},
Counter: &dto.Counter{
Value: proto.Float64(8),
},
},
},
}
scenarios := []struct {
format Format
expectMetricName string
expectLabelName string
}{
{
format: FmtProtoDelim,
expectMetricName: "foo_metric",
expectLabelName: "dotted_label_name",
},
{
format: FmtProtoDelim.WithEscapingScheme(model.NoEscaping),
expectMetricName: "foo.metric",
expectLabelName: "dotted.label.name",
},
{
format: FmtProtoDelim.WithEscapingScheme(model.DotsEscaping),
expectMetricName: "foo_dot_metric",
expectLabelName: "dotted_dot_label_dot_name",
},
{
format: FmtText,
expectMetricName: "foo_metric",
expectLabelName: "dotted_label_name",
},
{
format: FmtText.WithEscapingScheme(model.NoEscaping),
expectMetricName: "foo.metric",
expectLabelName: "dotted.label.name",
},
{
format: FmtText.WithEscapingScheme(model.DotsEscaping),
expectMetricName: "foo_dot_metric",
expectLabelName: "dotted_dot_label_dot_name",
},
// common library does not support proto text or open metrics parsing so we
// do not test those here.
}
for i, scenario := range scenarios {
out := bytes.NewBuffer(make([]byte, 0))
enc := NewEncoder(out, scenario.format)
err := enc.Encode(metric)
if err != nil {
t.Errorf("%d. error: %s", i, err)
continue
}
dec := NewDecoder(bytes.NewReader(out.Bytes()), scenario.format)
var gotFamily dto.MetricFamily
err = dec.Decode(&gotFamily)
if err != nil {
t.Errorf("%v: unexpected error during decode: %s", scenario.format, err.Error())
}
if gotFamily.GetName() != scenario.expectMetricName {
t.Errorf("%v: incorrect encoded metric name, want %v, got %v", scenario.format, scenario.expectMetricName, gotFamily.GetName())
}
lName := gotFamily.GetMetric()[1].Label[0].GetName()
if lName != scenario.expectLabelName {
t.Errorf("%v: incorrect encoded label name, want %v, got %v", scenario.format, scenario.expectLabelName, lName)
}
}
}