Skip to content

Commit e55d2de

Browse files
authored
chore: support dots in label names (#3335)
1 parent eaa53a2 commit e55d2de

File tree

2 files changed

+95
-8
lines changed

2 files changed

+95
-8
lines changed

pkg/validation/validate.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,21 +123,57 @@ func ValidateLabels(limits LabelValidationLimits, tenantID string, ls []*typesv1
123123
for _, l := range ls {
124124
if len(l.Name) > limits.MaxLabelNameLength(tenantID) {
125125
return NewErrorf(LabelNameTooLong, LabelNameTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Name)
126-
} else if len(l.Value) > limits.MaxLabelValueLength(tenantID) {
126+
}
127+
if len(l.Value) > limits.MaxLabelValueLength(tenantID) {
127128
return NewErrorf(LabelValueTooLong, LabelValueTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Value)
128-
} else if !model.LabelName(l.Name).IsValid() {
129-
return NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label name '"+l.Name+"'")
130-
} else if !model.LabelValue(l.Value).IsValid() {
129+
}
130+
var origName string
131+
var ok bool
132+
if origName, l.Name, ok = SanitizeLabelName(l.Name); !ok {
133+
return NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label name '"+origName+"'")
134+
}
135+
if !model.LabelValue(l.Value).IsValid() {
131136
return NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label value '"+l.Value+"'")
132-
} else if cmp := strings.Compare(lastLabelName, l.Name); cmp == 0 {
133-
return NewErrorf(DuplicateLabelNames, DuplicateLabelNamesErrorMsg, phlaremodel.LabelPairsString(ls), l.Name)
137+
}
138+
if cmp := strings.Compare(lastLabelName, l.Name); cmp == 0 {
139+
return NewErrorf(DuplicateLabelNames, DuplicateLabelNamesErrorMsg, phlaremodel.LabelPairsString(ls), origName)
134140
}
135141
lastLabelName = l.Name
136142
}
137143

138144
return nil
139145
}
140146

147+
// SanitizeLabelName reports whether the label name is valid,
148+
// and returns the sanitized value.
149+
//
150+
// The only change the function makes is replacing dots with underscores.
151+
func SanitizeLabelName(ln string) (old, sanitized string, ok bool) {
152+
if len(ln) == 0 {
153+
return ln, ln, false
154+
}
155+
hasDots := false
156+
for i, b := range ln {
157+
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) {
158+
if b == '.' {
159+
hasDots = true
160+
} else {
161+
return ln, ln, false
162+
}
163+
}
164+
}
165+
if !hasDots {
166+
return ln, ln, true
167+
}
168+
r := []rune(ln)
169+
for i, b := range r {
170+
if b == '.' {
171+
r[i] = '_'
172+
}
173+
}
174+
return ln, string(r), true
175+
}
176+
141177
type ProfileValidationLimits interface {
142178
MaxProfileSizeBytes(tenantID string) int
143179
MaxProfileStacktraceSamples(tenantID string) int

pkg/validation/validate_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/prometheus/common/model"
8+
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910

1011
googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
@@ -50,7 +51,7 @@ func TestValidateLabels(t *testing.T) {
5051
{Name: "foo3", Value: "bar"},
5152
{Name: "foo4", Value: "bar"},
5253
},
53-
expectedErr: `profile series '{foo1="bar", foo2="bar", foo3="bar", foo4="bar", service_name="svc"}' has 5 label names; limit 3`,
54+
expectedErr: `profile series '{foo1="bar", foo2="bar", foo3="bar", foo4="bar", service_name="svc"}' has 5 label names; limit 4`,
5455
expectedReason: MaxLabelNamesPerSeries,
5556
},
5657
{
@@ -112,10 +113,22 @@ func TestValidateLabels(t *testing.T) {
112113
expectedReason: DuplicateLabelNames,
113114
expectedErr: "profile with labels '{__name__=\"qux\", service_name=\"svc\", service_name=\"svc\"}' has duplicate label name: 'service_name'",
114115
},
116+
117+
{
118+
name: "dupe sanitized",
119+
lbs: []*typesv1.LabelPair{
120+
{Name: model.MetricNameLabel, Value: "qux"},
121+
{Name: "label.name", Value: "foo"},
122+
{Name: "label.name", Value: "bar"},
123+
{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
124+
},
125+
expectedReason: DuplicateLabelNames,
126+
expectedErr: "profile with labels '{__name__=\"qux\", label_name=\"foo\", label_name=\"bar\", service_name=\"svc\"}' has duplicate label name: 'label.name'",
127+
},
115128
} {
116129
t.Run(tt.name, func(t *testing.T) {
117130
err := ValidateLabels(MockLimits{
118-
MaxLabelNamesPerSeriesValue: 3,
131+
MaxLabelNamesPerSeriesValue: 4,
119132
MaxLabelNameLengthValue: 12,
120133
MaxLabelValueLengthValue: 10,
121134
}, "foo", tt.lbs)
@@ -400,3 +413,41 @@ func TestValidateFlamegraphMaxNodes(t *testing.T) {
400413
})
401414
}
402415
}
416+
417+
func Test_SanitizeLabelName(t *testing.T) {
418+
for _, tc := range []struct {
419+
input string
420+
expected string
421+
valid bool
422+
}{
423+
{"", "", false},
424+
{".", "_", true},
425+
{".a", "_a", true},
426+
{"a.", "a_", true},
427+
{"..", "__", true},
428+
{"..a", "__a", true},
429+
{"a..", "a__", true},
430+
{"a.a", "a_a", true},
431+
{".a.", "_a_", true},
432+
{"..a..", "__a__", true},
433+
{"世界", "世界", false},
434+
{"界世_a", "界世_a", false},
435+
{"界世__a", "界世__a", false},
436+
{"a_世界", "a_世界", false},
437+
{"0.a", "0.a", false},
438+
{"0a", "0a", false},
439+
{"a.0", "a_0", true},
440+
{"a0", "a0", true},
441+
{"_", "_", true},
442+
{"__a", "__a", true},
443+
{"__a__", "__a__", true},
444+
} {
445+
tc := tc
446+
t.Run("", func(t *testing.T) {
447+
origName, actual, valid := SanitizeLabelName(tc.input)
448+
assert.Equal(t, tc.input, origName)
449+
assert.Equal(t, tc.expected, actual)
450+
assert.Equal(t, tc.valid, valid)
451+
})
452+
}
453+
}

0 commit comments

Comments
 (0)