mirror of
https://github.com/hanzoai/common.git
synced 2026-10-30 18:44:05 +00:00
Module path renamed github.com/prometheus/common → github.com/hanzoai/common. The only functional patch is config/http_config.go: strip conntrack-based dial-context instrumentation, replace with stdlib net.Dialer. This breaks the transitive chain prometheus/common/config → mwitkow/go-conntrack → prometheus/client_golang/prometheus that was leaking into every consumer of prometheus/common/config (alertmanager notifiers, etc.). Functional impact: HTTP clients built via NewClientFromConfig no longer emit go-conntrack connection-tracking metrics (active/closed counters). The connection count is observable via the underlying http.Transport metrics that luxfi/metric ships, so this is loss-of-observability-zero for callers on the Hanzo/Lux stack.
897 lines
22 KiB
Go
897 lines
22 KiB
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 (
|
|
"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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|