blob: 8a8bec95bc8b86c749bb5f87ffa7e1fd8ead4bac [file]
// Copyright 2013 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 model
import (
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v2"
"google.golang.org/protobuf/proto"
)
func testMetric(t testing.TB) {
scenarios := []struct {
input LabelSet
fingerprint Fingerprint
fastFingerprint Fingerprint
}{
{
input: LabelSet{},
fingerprint: 14695981039346656037,
fastFingerprint: 14695981039346656037,
},
{
input: LabelSet{
"first_name": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
},
fingerprint: 5911716720268894962,
fastFingerprint: 11310079640881077873,
},
{
input: LabelSet{
"x": "y",
},
fingerprint: 8241431561484471700,
fastFingerprint: 13948396922932177635,
},
{
input: LabelSet{
"a": "bb",
"b": "c",
},
fingerprint: 3016285359649981711,
fastFingerprint: 3198632812309449502,
},
{
input: LabelSet{
"a": "b",
"bb": "c",
},
fingerprint: 7122421792099404749,
fastFingerprint: 5774953389407657638,
},
}
for i, scenario := range scenarios {
input := Metric(scenario.input)
if scenario.fingerprint != input.Fingerprint() {
t.Errorf("%d. expected %d, got %d", i, scenario.fingerprint, input.Fingerprint())
}
if scenario.fastFingerprint != input.FastFingerprint() {
t.Errorf("%d. expected %d, got %d", i, scenario.fastFingerprint, input.FastFingerprint())
}
}
}
func TestMetric(t *testing.T) {
testMetric(t)
}
func BenchmarkMetric(b *testing.B) {
for i := 0; i < b.N; i++ {
testMetric(b)
}
}
func TestValidationScheme(t *testing.T) {
var scheme ValidationScheme
require.Equal(t, UnsetValidation, scheme)
}
func TestValidationScheme_String(t *testing.T) {
for _, tc := range []struct {
name string
scheme ValidationScheme
want string
}{
{
name: "Unset",
scheme: UnsetValidation,
want: "unset",
},
{
name: "Legacy",
scheme: LegacyValidation,
want: "legacy",
},
{
name: "UTF8",
scheme: UTF8Validation,
want: "utf8",
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, tc.scheme.String())
})
}
}
func TestValidationScheme_MarshalYAML(t *testing.T) {
for _, tc := range []struct {
name string
scheme ValidationScheme
want string
}{
{
name: "Unset",
scheme: UnsetValidation,
want: `""`,
},
{
name: "Legacy",
scheme: LegacyValidation,
want: "legacy",
},
{
name: "UTF8",
scheme: UTF8Validation,
want: "utf8",
},
} {
t.Run(tc.name, func(t *testing.T) {
marshaled, err := yaml.Marshal(tc.scheme)
require.NoError(t, err)
require.Equal(t, tc.want, strings.TrimSpace(string(marshaled)))
})
}
}
func TestValidationScheme_UnmarshalYAML(t *testing.T) {
for _, tc := range []struct {
name string
input string
want ValidationScheme
wantError error
}{
{
name: "Unset empty input",
input: "",
want: UnsetValidation,
},
{
name: "Unset quoted input",
input: `""`,
want: UnsetValidation,
},
{
name: "Legacy",
input: "legacy",
want: LegacyValidation,
},
{
name: "UTF8",
input: "utf8",
want: UTF8Validation,
},
{
name: "Invalid",
input: "invalid",
wantError: errors.New(`unrecognized ValidationScheme: "invalid"`),
},
} {
t.Run(tc.name, func(t *testing.T) {
scheme := UnsetValidation
err := yaml.Unmarshal([]byte(tc.input), &scheme)
if tc.wantError == nil {
require.NoError(t, err)
require.Equal(t, tc.want, scheme)
} else {
require.EqualError(t, err, tc.wantError.Error())
}
})
}
}
func TestValidationScheme_UnmarshalJSON(t *testing.T) {
testCases := []struct {
name string
input string
want ValidationScheme
wantErr bool
}{
{
name: "invalid",
input: `invalid`,
wantErr: true,
},
{
name: "empty",
input: `""`,
want: UnsetValidation,
},
{
name: "legacy validation",
input: `"legacy"`,
want: LegacyValidation,
},
{
name: "utf8 validation",
input: `"utf8"`,
want: UTF8Validation,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var got ValidationScheme
err := json.Unmarshal([]byte(tc.input), &got)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.want, got)
output, err := json.Marshal(got)
require.NoError(t, err)
require.Equal(t, tc.input, string(output))
})
}
}
func TestValidationScheme_Set(t *testing.T) {
testCases := []struct {
name string
input string
want ValidationScheme
wantErr bool
}{
{
name: "invalid",
input: `invalid`,
wantErr: true,
},
{
name: "empty",
input: ``,
want: UnsetValidation,
},
{
name: "legacy validation",
input: `legacy`,
want: LegacyValidation,
},
{
name: "utf8 validation",
input: `utf8`,
want: UTF8Validation,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var got ValidationScheme
err := got.Set(tc.input)
if tc.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tc.want, got)
})
}
}
func TestValidationScheme_IsMetricNameValid(t *testing.T) {
scenarios := []struct {
mn string
legacyValid bool
utf8Valid bool
}{
{
mn: "Avalid_23name",
legacyValid: true,
utf8Valid: true,
},
{
mn: "_Avalid_23name",
legacyValid: true,
utf8Valid: true,
},
{
mn: "1valid_23name",
legacyValid: false,
utf8Valid: true,
},
{
mn: "avalid_23name",
legacyValid: true,
utf8Valid: true,
},
{
mn: "Ava:lid_23name",
legacyValid: true,
utf8Valid: true,
},
{
mn: "a lid_23name",
legacyValid: false,
utf8Valid: true,
},
{
mn: ":leading_colon",
legacyValid: true,
utf8Valid: true,
},
{
mn: "colon:in:the:middle",
legacyValid: true,
utf8Valid: true,
},
{
mn: "",
legacyValid: false,
utf8Valid: false,
},
{
mn: "a\xc5z",
legacyValid: false,
utf8Valid: false,
},
{
mn: "dot.in.name",
legacyValid: false,
utf8Valid: true,
},
}
for _, s := range scenarios {
t.Run(fmt.Sprintf("%s,%t,%t", s.mn, s.legacyValid, s.utf8Valid), func(t *testing.T) {
if LegacyValidation.IsValidMetricName(s.mn) != s.legacyValid {
t.Errorf("Expected %v for %q using LegacyValidation.IsValidMetricName", s.legacyValid, s.mn)
}
if MetricNameRE.MatchString(s.mn) != s.legacyValid {
t.Errorf("Expected %v for %q using regexp matching", s.legacyValid, s.mn)
}
if UTF8Validation.IsValidMetricName(s.mn) != s.utf8Valid {
t.Errorf("Expected %v for %q using UTF8Validation.IsValidMetricName", s.utf8Valid, s.mn)
}
// Test deprecated functions.
if IsValidLegacyMetricName(s.mn) != s.legacyValid {
t.Errorf("Expected %v for %q using IsValidLegacyMetricNames", s.legacyValid, s.mn)
}
origScheme := NameValidationScheme
t.Cleanup(func() {
NameValidationScheme = origScheme
})
NameValidationScheme = LegacyValidation
if IsValidMetricName(LabelValue(s.mn)) != s.legacyValid {
t.Errorf("Expected %v for %q using legacy IsValidMetricName", s.legacyValid, s.mn)
}
NameValidationScheme = UTF8Validation
if IsValidMetricName(LabelValue(s.mn)) != s.utf8Valid {
t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.utf8Valid, s.mn)
}
})
}
}
func TestMetricClone(t *testing.T) {
m := Metric{
"first_name": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
}
m2 := m.Clone()
if len(m) != len(m2) {
t.Errorf("expected the length of the cloned metric to be equal to the input metric")
}
for ln, lv := range m2 {
expected := m[ln]
if expected != lv {
t.Errorf("expected label value %s but got %s for label name %s", expected, lv, ln)
}
}
}
func TestMetricToString(t *testing.T) {
scenarios := []struct {
name string
input Metric
expected string
}{
{
name: "valid metric without __name__ label",
input: Metric{
"first_name": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
},
expected: `{first_name="electro", manufacturer="westinghouse", occupation="robot"}`,
},
{
name: "valid metric with __name__ label",
input: Metric{
"__name__": "electro",
"occupation": "robot",
"manufacturer": "westinghouse",
},
expected: `electro{manufacturer="westinghouse", occupation="robot"}`,
},
{
name: "empty metric with __name__ label",
input: Metric{
"__name__": "fooname",
},
expected: "fooname",
},
{
name: "empty metric",
input: Metric{},
expected: "{}",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
actual := scenario.input.String()
if actual != scenario.expected {
t.Errorf("expected string output %s but got %s", scenario.expected, actual)
}
})
}
}
func TestEscapeName(t *testing.T) {
scenarios := []struct {
name string
input string
expectedUnderscores string
expectedDots string
expectedUnescapedDots string
expectedValue string
}{
{
name: "empty string",
},
{
name: "legacy valid name",
input: "no:escaping_required",
expectedUnderscores: "no:escaping_required",
// Dots escaping will escape underscores even though it's not strictly
// necessary for compatibility.
expectedDots: "no:escaping__required",
expectedUnescapedDots: "no:escaping_required",
expectedValue: "no:escaping_required",
},
{
name: "name with dots",
input: "mysystem.prod.west.cpu.load",
expectedUnderscores: "mysystem_prod_west_cpu_load",
expectedDots: "mysystem_dot_prod_dot_west_dot_cpu_dot_load",
expectedUnescapedDots: "mysystem.prod.west.cpu.load",
expectedValue: "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load",
},
{
name: "name with dots and underscore",
input: "mysystem.prod.west.cpu.load_total",
expectedUnderscores: "mysystem_prod_west_cpu_load_total",
expectedDots: "mysystem_dot_prod_dot_west_dot_cpu_dot_load__total",
expectedUnescapedDots: "mysystem.prod.west.cpu.load_total",
expectedValue: "U__mysystem_2e_prod_2e_west_2e_cpu_2e_load__total",
},
{
name: "name with dots and colon",
input: "http.status:sum",
expectedUnderscores: "http_status:sum",
expectedDots: "http_dot_status:sum",
expectedUnescapedDots: "http.status:sum",
expectedValue: "U__http_2e_status:sum",
},
{
name: "name with spaces and emoji",
input: "label with 😱",
expectedUnderscores: "label_with__",
expectedDots: "label__with____",
expectedUnescapedDots: "label_with__",
expectedValue: "U__label_20_with_20__1f631_",
},
{
name: "name with unicode characters > 0x100",
input: "花火",
expectedUnderscores: "__",
expectedDots: "____",
// Dots-replacement does not know the difference between two replaced
// characters and a single underscore.
expectedUnescapedDots: "__",
expectedValue: "U___82b1__706b_",
},
{
name: "name with spaces and edge-case value",
input: "label with \u0100",
expectedUnderscores: "label_with__",
expectedDots: "label__with____",
expectedUnescapedDots: "label_with__",
expectedValue: "U__label_20_with_20__100_",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
got := EscapeName(scenario.input, UnderscoreEscaping)
if got != scenario.expectedUnderscores {
t.Errorf("expected string output %s but got %s", scenario.expectedUnderscores, got)
}
// Unescaping with the underscore method is a noop.
got = UnescapeName(got, UnderscoreEscaping)
if got != scenario.expectedUnderscores {
t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnderscores, got)
}
got = EscapeName(scenario.input, DotsEscaping)
if got != scenario.expectedDots {
t.Errorf("expected string output %s but got %s", scenario.expectedDots, got)
}
got = UnescapeName(got, DotsEscaping)
if got != scenario.expectedUnescapedDots {
t.Errorf("expected unescaped string output %s but got %s", scenario.expectedUnescapedDots, got)
}
got = EscapeName(scenario.input, ValueEncodingEscaping)
if got != scenario.expectedValue {
t.Errorf("expected string output %s but got %s", scenario.expectedValue, got)
}
// Unescaped result should always be identical to the original input.
got = UnescapeName(got, ValueEncodingEscaping)
if got != scenario.input {
t.Errorf("expected unescaped string output %s but got %s", scenario.input, got)
}
})
}
}
func TestValueUnescapeErrors(t *testing.T) {
scenarios := []struct {
name string
input string
expected string
}{
{
name: "empty string",
},
{
name: "basic case, no error",
input: "U__no:unescapingrequired",
expected: "no:unescapingrequired",
},
{
name: "capitals ok, no error",
input: "U__capitals_2E_ok",
expected: "capitals.ok",
},
{
name: "underscores, no error",
input: "U__underscores__doubled__",
expected: "underscores_doubled_",
},
{
name: "invalid single underscore",
input: "U__underscores_doubled_",
expected: "U__underscores_doubled_",
},
{
name: "invalid single underscore, 2",
input: "U__underscores__doubled_",
expected: "U__underscores__doubled_",
},
{
name: "giant fake utf-8 code",
input: "U__my__hack_2e_attempt_872348732fabdabbab_",
expected: "U__my__hack_2e_attempt_872348732fabdabbab_",
},
{
name: "trailing utf-8",
input: "U__my__hack_2e",
expected: "U__my__hack_2e",
},
{
name: "invalid utf-8 value",
input: "U__bad__utf_2eg_",
expected: "U__bad__utf_2eg_",
},
{
name: "surrogate utf-8 value",
input: "U__bad__utf_D900_",
expected: "U__bad__utf_D900_",
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
got := UnescapeName(scenario.input, ValueEncodingEscaping)
if got != scenario.expected {
t.Errorf("expected unescaped string output %s but got %s", scenario.expected, got)
}
})
}
}
func TestEscapeMetricFamily(t *testing.T) {
scenarios := []struct {
name string
input *dto.MetricFamily
scheme EscapingScheme
expected *dto.MetricFamily
}{
{
name: "empty",
input: &dto.MetricFamily{},
scheme: ValueEncodingEscaping,
expected: &dto.MetricFamily{},
},
{
name: "simple, no escaping needed",
scheme: ValueEncodingEscaping,
input: &dto.MetricFamily{
Name: proto.String("my_metric"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("my_metric"),
},
{
Name: proto.String("some_label"),
Value: proto.String("labelvalue"),
},
},
},
},
},
expected: &dto.MetricFamily{
Name: proto.String("my_metric"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("my_metric"),
},
{
Name: proto.String("some_label"),
Value: proto.String("labelvalue"),
},
},
},
},
},
},
{
name: "label name escaping needed",
scheme: ValueEncodingEscaping,
input: &dto.MetricFamily{
Name: proto.String("my_metric"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("my_metric"),
},
{
Name: proto.String("some.label"),
Value: proto.String("labelvalue"),
},
},
},
},
},
expected: &dto.MetricFamily{
Name: proto.String("my_metric"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("my_metric"),
},
{
Name: proto.String("U__some_2e_label"),
Value: proto.String("labelvalue"),
},
},
},
},
},
},
{
name: "counter, escaping needed",
scheme: ValueEncodingEscaping,
input: &dto.MetricFamily{
Name: proto.String("my.metric"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("my.metric"),
},
{
Name: proto.String("some?label"),
Value: proto.String("label??value"),
},
},
},
},
},
expected: &dto.MetricFamily{
Name: proto.String("U__my_2e_metric"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("U__my_2e_metric"),
},
{
Name: proto.String("U__some_3f_label"),
Value: proto.String("label??value"),
},
},
},
},
},
},
{
name: "gauge, escaping needed",
scheme: DotsEscaping,
input: &dto.MetricFamily{
Name: proto.String("unicode.and.dots.花火"),
Help: proto.String("some help text"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
{
Gauge: &dto.Gauge{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("unicode.and.dots.花火"),
},
{
Name: proto.String("some_label"),
Value: proto.String("label??value"),
},
},
},
},
},
expected: &dto.MetricFamily{
Name: proto.String("unicode_dot_and_dot_dots_dot_____"),
Help: proto.String("some help text"),
Type: dto.MetricType_GAUGE.Enum(),
Metric: []*dto.Metric{
{
Gauge: &dto.Gauge{
Value: proto.Float64(34.2),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("unicode_dot_and_dot_dots_dot_____"),
},
{
Name: proto.String("some_label"),
Value: proto.String("label??value"),
},
},
},
},
},
},
}
unexportList := []interface{}{dto.MetricFamily{}, dto.Metric{}, dto.LabelPair{}, dto.Counter{}, dto.Gauge{}}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
original := proto.Clone(scenario.input)
got := EscapeMetricFamily(scenario.input, scenario.scheme)
if !cmp.Equal(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)) {
t.Errorf("unexpected difference in escaped output:\n%s", cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)))
}
if !cmp.Equal(scenario.input, original, cmpopts.IgnoreUnexported(unexportList...)) {
t.Errorf("input was mutated during escaping:\n%s", cmp.Diff(scenario.expected, got, cmpopts.IgnoreUnexported(unexportList...)))
}
})
}
}
// TestProtoFormatUnchanged checks to see if the proto format changed, in which
// case EscapeMetricFamily will need to be updated.
func TestProtoFormatUnchanged(t *testing.T) {
scenarios := []struct {
name string
input proto.Message
expectFields []string
}{
{
name: "MetricFamily",
input: &dto.MetricFamily{},
expectFields: []string{"name", "help", "type", "metric", "unit"},
},
{
name: "Metric",
input: &dto.Metric{},
expectFields: []string{"label", "gauge", "counter", "summary", "untyped", "histogram", "timestamp_ms"},
},
{
name: "LabelPair",
input: &dto.LabelPair{},
expectFields: []string{"name", "value"},
},
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
desc := scenario.input.ProtoReflect().Descriptor()
fields := desc.Fields()
if fields.Len() != len(scenario.expectFields) {
t.Errorf("dto.MetricFamily changed length, expected %d, got %d", len(scenario.expectFields), fields.Len())
}
for i := 0; i < fields.Len(); i++ {
got := fields.Get(i).TextName()
if got != scenario.expectFields[i] {
t.Errorf("dto.MetricFamily field mismatch, expected %s got %s", scenario.expectFields[i], got)
}
}
})
}
}