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
81 changes: 78 additions & 3 deletions model/metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,75 @@ func IsValidLegacyMetricName(n string) bool {
return LegacyValidation.IsValidMetricName(n)
}

// IsValidLegacyLabelName reports whether n is a valid label name under the
// legacy Prometheus data model. Unlike metric names, label names must not
// contain ':'.
func IsValidLegacyLabelName(n string) bool {
return LegacyValidation.IsValidLabelName(n)
}

// EscapeLabelName escapes the incoming label name according to the provided
// escaping scheme. It behaves like EscapeName but applies label-name rules:
// ':' is not a valid character in a label name and is always escaped.
func EscapeLabelName(name string, scheme EscapingScheme) string {
if len(name) == 0 {
return name
}
var escaped strings.Builder
switch scheme {
case NoEscaping:
return name
case UnderscoreEscaping:
if IsValidLegacyLabelName(name) {
return name
}
for i, b := range name {
if isValidLegacyLabelRune(b, i) {
escaped.WriteRune(b)
} else {
escaped.WriteRune('_')
}
}
return escaped.String()
case DotsEscaping:
for i, b := range name {
switch {
case b == '_':
escaped.WriteString("__")
case b == '.':
escaped.WriteString("_dot_")
case isValidLegacyLabelRune(b, i):
escaped.WriteRune(b)
default:
escaped.WriteString("__")
}
}
return escaped.String()
case ValueEncodingEscaping:
if IsValidLegacyLabelName(name) {
return name
}
escaped.WriteString("U__")
for i, b := range name {
switch {
case b == '_':
escaped.WriteString("__")
case isValidLegacyLabelRune(b, i):
escaped.WriteRune(b)
case !utf8.ValidRune(b):
escaped.WriteString("_FFFD_")
default:
escaped.WriteRune('_')
escaped.WriteString(strconv.FormatInt(int64(b), 16))
escaped.WriteRune('_')
}
}
return escaped.String()
default:
panic(fmt.Sprintf("invalid escaping scheme %d", scheme))
}
}

// EscapeMetricFamily escapes the given metric names and labels with the given
// escaping scheme. Returns a new object that uses the same pointers to fields
// when possible and creates new escaped versions so as not to mutate the
Expand Down Expand Up @@ -373,12 +442,12 @@ func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricF
})
continue
}
if l.Name == nil || IsValidLegacyMetricName(l.GetName()) {
if l.Name == nil || IsValidLegacyLabelName(l.GetName()) {
escaped.Label = append(escaped.Label, l)
continue
}
escaped.Label = append(escaped.Label, &dto.LabelPair{
Name: proto.String(EscapeName(l.GetName(), scheme)),
Name: proto.String(EscapeLabelName(l.GetName(), scheme)),
Value: l.Value,
})
}
Expand All @@ -392,7 +461,7 @@ func metricNeedsEscaping(m *dto.Metric) bool {
if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(l.GetValue()) {
return true
}
if !IsValidLegacyMetricName(l.GetName()) {
if !IsValidLegacyLabelName(l.GetName()) {
return true
}
}
Expand Down Expand Up @@ -550,6 +619,12 @@ func isValidLegacyRune(b rune, i int) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)
}

// isValidLegacyLabelRune is like isValidLegacyRune but excludes ':'.
// Colons are reserved for metric names only; they have never been valid in label names.
func isValidLegacyLabelRune(b rune, i int) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)
}

func (e EscapingScheme) String() string {
switch e {
case NoEscaping:
Expand Down
109 changes: 109 additions & 0 deletions model/metric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,67 @@ func TestEscapeName(t *testing.T) {
}
}

func TestEscapeLabelName(t *testing.T) {
scenarios := []struct {
name string
input string
expectedUnderscores string
expectedDots string
expectedValue string
}{
{
name: "empty string",
},
{
name: "legacy valid label name, no escaping required",
input: "no_escaping_required",
expectedUnderscores: "no_escaping_required",
expectedDots: "no__escaping__required",
expectedValue: "no_escaping_required",
},
{
name: "colon only valid in metric names, not label names",
input: "app:instance_id",
expectedUnderscores: "app_instance_id",
expectedDots: "app__instance__id",
expectedValue: "U__app_3a_instance__id",
},
{
name: "colon and hyphen both escaped in label names",
input: "app:instance-id",
expectedUnderscores: "app_instance_id",
expectedDots: "app__instance__id",
expectedValue: "U__app_3a_instance_2d_id",
},
{
name: "dot and colon both escaped in label names",
input: "http.status:sum",
expectedUnderscores: "http_status_sum",
expectedDots: "http_dot_status__sum",
expectedValue: "U__http_2e_status_3a_sum",
},
}

for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
got := EscapeLabelName(scenario.input, UnderscoreEscaping)
if got != scenario.expectedUnderscores {
t.Errorf("UnderscoreEscaping: expected %q but got %q", scenario.expectedUnderscores, got)
}

got = EscapeLabelName(scenario.input, DotsEscaping)
if got != scenario.expectedDots {
t.Errorf("DotsEscaping: expected %q but got %q", scenario.expectedDots, got)
}

got = EscapeLabelName(scenario.input, ValueEncodingEscaping)
if got != scenario.expectedValue {
t.Errorf("ValueEncodingEscaping: expected %q but got %q", scenario.expectedValue, got)
}
})
}
}

func TestValueUnescapeErrors(t *testing.T) {
scenarios := []struct {
name string
Expand Down Expand Up @@ -794,6 +855,54 @@ func TestEscapeMetricFamily(t *testing.T) {
},
},
},
{
name: "colon in label name is escaped, colon in metric name is not",
scheme: UnderscoreEscaping,
input: &dto.MetricFamily{
Name: proto.String("requests:total"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(1),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("requests:total"),
},
{
Name: proto.String("app:instance_id"),
Value: proto.String("srv1"),
},
},
},
},
},
expected: &dto.MetricFamily{
Name: proto.String("requests:total"),
Help: proto.String("some help text"),
Type: dto.MetricType_COUNTER.Enum(),
Metric: []*dto.Metric{
{
Counter: &dto.Counter{
Value: proto.Float64(1),
},
Label: []*dto.LabelPair{
{
Name: proto.String("__name__"),
Value: proto.String("requests:total"),
},
{
Name: proto.String("app_instance_id"),
Value: proto.String("srv1"),
},
},
},
},
},
},
{
name: "gauge, escaping needed",
scheme: DotsEscaping,
Expand Down
Loading