Skip to content

Commit d410cdf

Browse files
authored
Fix: Handle duplicate validation correctly when sanitizing (#4238)
When replacing dots with underscores, we don't take care of the order of the labels slice, hence our duplicate check can be ineffective. This change will fix this bug and also will handle the case when both the sanitized and unsanitized label values actually match and reduce them to the sanitized variant. Discovered when reviewing #4236
1 parent bb8d879 commit d410cdf

File tree

2 files changed

+103
-23
lines changed

2 files changed

+103
-23
lines changed

pkg/validation/validate.go

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,23 @@ const (
6363
// Those profiles were dropped because of relabeling rules
6464
DroppedByRelabelRules Reason = "dropped_by_relabel_rules"
6565

66-
SeriesLimitErrorMsg = "Maximum active series limit exceeded (%d/%d), reduce the number of active streams (reduce labels or reduce label values), or contact your administrator to see if the limit can be increased"
67-
MissingLabelsErrorMsg = "error at least one label pair is required per profile"
68-
InvalidLabelsErrorMsg = "invalid labels '%s' with error: %s"
69-
MaxLabelNamesPerSeriesErrorMsg = "profile series '%s' has %d label names; limit %d"
70-
LabelNameTooLongErrorMsg = "profile with labels '%s' has label name too long: '%s'"
71-
LabelValueTooLongErrorMsg = "profile with labels '%s' has label value too long: '%s'"
72-
DuplicateLabelNamesErrorMsg = "profile with labels '%s' has duplicate label name: '%s'"
73-
QueryTooLongErrorMsg = "the query time range exceeds the limit (max_query_length, actual: %s, limit: %s)"
74-
ProfileTooBigErrorMsg = "the profile with labels '%s' exceeds the size limit (max_profile_size_byte, actual: %d, limit: %d)"
75-
ProfileTooManySamplesErrorMsg = "the profile with labels '%s' exceeds the samples count limit (max_profile_stacktrace_samples, actual: %d, limit: %d)"
76-
ProfileTooManySampleLabelsErrorMsg = "the profile with labels '%s' exceeds the sample labels limit (max_profile_stacktrace_sample_labels, actual: %d, limit: %d)"
77-
NotInIngestionWindowErrorMsg = "profile with labels '%s' is outside of ingestion window (profile timestamp: %s, %s)"
78-
MaxFlameGraphNodesErrorMsg = "max flamegraph nodes limit %d is greater than allowed %d"
79-
MaxFlameGraphNodesUnlimitedErrorMsg = "max flamegraph nodes limit must be set (max allowed %d)"
80-
QueryMissingTimeRangeErrorMsg = "missing time range in the query"
81-
QueryStartAfterEndErrorMsg = "query start time is after end time"
66+
SeriesLimitErrorMsg = "Maximum active series limit exceeded (%d/%d), reduce the number of active streams (reduce labels or reduce label values), or contact your administrator to see if the limit can be increased"
67+
MissingLabelsErrorMsg = "error at least one label pair is required per profile"
68+
InvalidLabelsErrorMsg = "invalid labels '%s' with error: %s"
69+
MaxLabelNamesPerSeriesErrorMsg = "profile series '%s' has %d label names; limit %d"
70+
LabelNameTooLongErrorMsg = "profile with labels '%s' has label name too long: '%s'"
71+
LabelValueTooLongErrorMsg = "profile with labels '%s' has label value too long: '%s'"
72+
DuplicateLabelNamesErrorMsg = "profile with labels '%s' has duplicate label name: '%s'"
73+
DuplicateLabelNamesAfterSanitizationErrorMsg = "profile with labels '%s' has duplicate label name '%s' after label name sanitization from '%s'"
74+
QueryTooLongErrorMsg = "the query time range exceeds the limit (max_query_length, actual: %s, limit: %s)"
75+
ProfileTooBigErrorMsg = "the profile with labels '%s' exceeds the size limit (max_profile_size_byte, actual: %d, limit: %d)"
76+
ProfileTooManySamplesErrorMsg = "the profile with labels '%s' exceeds the samples count limit (max_profile_stacktrace_samples, actual: %d, limit: %d)"
77+
ProfileTooManySampleLabelsErrorMsg = "the profile with labels '%s' exceeds the sample labels limit (max_profile_stacktrace_sample_labels, actual: %d, limit: %d)"
78+
NotInIngestionWindowErrorMsg = "profile with labels '%s' is outside of ingestion window (profile timestamp: %s, %s)"
79+
MaxFlameGraphNodesErrorMsg = "max flamegraph nodes limit %d is greater than allowed %d"
80+
MaxFlameGraphNodesUnlimitedErrorMsg = "max flamegraph nodes limit must be set (max allowed %d)"
81+
QueryMissingTimeRangeErrorMsg = "missing time range in the query"
82+
QueryStartAfterEndErrorMsg = "query start time is after end time"
8283
)
8384

8485
var (
@@ -128,32 +129,91 @@ func ValidateLabels(limits LabelValidationLimits, tenantID string, ls []*typesv1
128129
if !isValidServiceName(serviceNameValue) {
129130
return NewErrorf(MissingLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "service name is not provided")
130131
}
131-
lastLabelName := ""
132132

133-
for _, l := range ls {
133+
var (
134+
lastLabelName = ""
135+
idx = 0
136+
)
137+
for idx < len(ls) {
138+
l := ls[idx]
134139
if len(l.Name) > limits.MaxLabelNameLength(tenantID) {
135140
return NewErrorf(LabelNameTooLong, LabelNameTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Name)
136141
}
137142
if len(l.Value) > limits.MaxLabelValueLength(tenantID) {
138143
return NewErrorf(LabelValueTooLong, LabelValueTooLongErrorMsg, phlaremodel.LabelPairsString(ls), l.Value)
139144
}
140-
var origName string
141-
var ok bool
142-
if origName, l.Name, ok = SanitizeLabelName(l.Name); !ok {
145+
if origName, newName, ok := SanitizeLabelName(l.Name); ok && origName != newName {
146+
var err error
147+
ls, idx, err = handleSanitizedLabel(ls, idx, origName, newName)
148+
if err != nil {
149+
return err
150+
}
151+
lastLabelName = ""
152+
if idx > 0 && idx <= len(ls) {
153+
lastLabelName = ls[idx-1].Name
154+
}
155+
continue
156+
} else if !ok {
143157
return NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label name '"+origName+"'")
144158
}
145159
if !model.LabelValue(l.Value).IsValid() {
146160
return NewErrorf(InvalidLabels, InvalidLabelsErrorMsg, phlaremodel.LabelPairsString(ls), "invalid label value '"+l.Value+"'")
147161
}
148162
if cmp := strings.Compare(lastLabelName, l.Name); cmp == 0 {
149-
return NewErrorf(DuplicateLabelNames, DuplicateLabelNamesErrorMsg, phlaremodel.LabelPairsString(ls), origName)
163+
return NewErrorf(DuplicateLabelNames, DuplicateLabelNamesErrorMsg, phlaremodel.LabelPairsString(ls), l.Name)
150164
}
151165
lastLabelName = l.Name
166+
idx += 1
152167
}
153168

154169
return nil
155170
}
156171

172+
// handleSanitizedLabel handles the case where a label name is sanitized. It ensures that the label name is unique and fails if the value is distinct.
173+
func handleSanitizedLabel(ls []*typesv1.LabelPair, origIdx int, origName, newName string) ([]*typesv1.LabelPair, int, error) {
174+
var (
175+
found = false
176+
origValue = ls[origIdx].Value
177+
newIdx int
178+
)
179+
180+
for idx := range ls {
181+
// label name matches
182+
if ls[idx].Name == newName {
183+
if ls[idx].Value == origValue {
184+
found = true
185+
} else {
186+
return ls, origIdx, NewErrorf(DuplicateLabelNames, DuplicateLabelNamesAfterSanitizationErrorMsg, phlaremodel.LabelPairsString(ls), newName, origName)
187+
}
188+
}
189+
// move up labels that are after the original index
190+
if idx > origIdx {
191+
idx -= 1
192+
ls[idx] = ls[idx+1]
193+
}
194+
// check if the new label should be inserted before the current label
195+
if newName < ls[idx].Name {
196+
newIdx = idx
197+
}
198+
}
199+
200+
// when the new label already exists with matchin value, we don't need to insert it and we continue at the original index
201+
if found {
202+
return ls[:len(ls)-1], origIdx, nil
203+
}
204+
205+
// insert the new label at the determined index
206+
copy(ls[newIdx+1:], ls[newIdx:])
207+
ls[newIdx] = &typesv1.LabelPair{Name: newName, Value: origValue}
208+
209+
// if the new label is inserted after the original index, we need to return the original index, so all labels after the original index are validated
210+
if newIdx > origIdx {
211+
return ls, origIdx, nil
212+
}
213+
214+
return ls, newIdx, nil
215+
}
216+
157217
// SanitizeLabelName reports whether the label name is valid,
158218
// and returns the sanitized value.
159219
//

pkg/validation/validate_test.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,27 @@ func TestValidateLabels(t *testing.T) {
123123
{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
124124
},
125125
expectedReason: DuplicateLabelNames,
126-
expectedErr: "profile with labels '{__name__=\"qux\", label_name=\"foo\", label_name=\"bar\", service_name=\"svc\"}' has duplicate label name: 'label.name'",
126+
expectedErr: "profile with labels '{__name__=\"qux\", label.name=\"bar\", label_name=\"foo\", service_name=\"svc\"}' has duplicate label name 'label_name' after label name sanitization from 'label.name'",
127+
},
128+
{
129+
name: "duplicates once sanitized with matching values",
130+
lbs: []*typesv1.LabelPair{
131+
{Name: model.MetricNameLabel, Value: "qux"},
132+
{Name: "service.name", Value: "svc0"},
133+
{Name: "service_abc", Value: "def"},
134+
{Name: "service_name", Value: "svc0"},
135+
},
136+
},
137+
{
138+
name: "duplicates once sanitized with conflicting values",
139+
lbs: []*typesv1.LabelPair{
140+
{Name: model.MetricNameLabel, Value: "qux"},
141+
{Name: "service.name", Value: "svc1"},
142+
{Name: "service_abc", Value: "def"},
143+
{Name: "service_name", Value: "svc0"},
144+
},
145+
expectedReason: DuplicateLabelNames,
146+
expectedErr: "profile with labels '{__name__=\"qux\", service_abc=\"def\", service_abc=\"def\", service_name=\"svc0\"}' has duplicate label name 'service_name' after label name sanitization from 'service.name'",
127147
},
128148
} {
129149
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)