File: //proc/thread-self/root/opt/go/pkg/mod/github.com/prometheus/
[email protected]/model/metric_test.go
// 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 (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
dto "github.com/prometheus/client_model/go"
"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 TestMetricNameIsLegacyValid(t *testing.T) {
scenarios := []struct {
mn LabelValue
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,
},
}
for _, s := range scenarios {
NameValidationScheme = LegacyValidation
if IsValidMetricName(s.mn) != s.legacyValid {
t.Errorf("Expected %v for %q using legacy IsValidMetricName method", s.legacyValid, s.mn)
}
if MetricNameRE.MatchString(string(s.mn)) != s.legacyValid {
t.Errorf("Expected %v for %q using regexp matching", s.legacyValid, s.mn)
}
NameValidationScheme = UTF8Validation
if IsValidMetricName(s.mn) != s.utf8Valid {
t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.legacyValid, 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)
}
}
})
}
}