fix(go/plugin/chartengine): sanitize label value dots in chart IDs (#21859)
Ilya Mashchenko committed
Mar 2, 2026 at 13:31 UTC
c1bc28e77b22cb7c0605db93dad9d4b1f637a2e4
4 files changed
+140
-14
src/go/plugin/framework/chartengine/autogen.go
+15
-8
@@ -495,20 +495,27 @@ func buildJoinedLabelAutogenID(metricName string, labels metrix.LabelView, exclu
495
}
496
497
func normalizeAutogenLabelValue(value string) string {
498
- if strings.IndexByte(value, ' ') != -1 {
499
- value = strings.ReplaceAll(value, " ", "_")
500
- }
498
if strings.IndexByte(value, '\\') != -1 {
502
- if v := decodeAutogenLabelValue(value); strings.IndexByte(v, '\\') != -1 {
499
+ v := decodeAutogenLabelValue(value)
500
+ switch {
501
+ case strings.IndexByte(v, '\\') != -1:
502
value = strings.ReplaceAll(v, `\`, "_")
504
- } else {
503
+ case hasControlChars(v):
504
+ // Keep raw when unquoting introduces controls (e.g. \b => backspace).
505
+ default:
506
value = v
507
}
508
}
508
- if strings.IndexByte(value, '\'') != -1 {
509
- value = strings.ReplaceAll(value, "'", "")
509
+ return sanitizeChartIDLabelValue(value)
510
+}
511
+
512
+func hasControlChars(value string) bool {
513
+ for _, r := range value {
514
+ if r < 0x20 || r == 0x7f {
515
+ return true
516
+ }
517
}
511
- return value
518
+ return false
519
}
520
521
func decodeAutogenLabelValue(value string) string {
src/go/plugin/framework/chartengine/autogen_test.go
+43
@@ -260,3 +260,46 @@ func sortedLabelView(labels map[string]string) metrix.LabelView {
260
})
261
return labelSliceView{items: items}
262
}
263
+
264
+func TestBuildScalarAutogenRouteSanitizesDotLabelValues(t *testing.T) {
265
+ route, ok, err := buildScalarAutogenRoute(
266
+ "svc.requests_total",
267
+ sortedLabelView(map[string]string{
268
+ "instance": "db1.eu",
269
+ "job": "mysql.prod",
270
+ }),
271
+ metrix.SeriesMeta{Kind: metrix.MetricKindCounter},
272
+ AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
273
+ "",
274
+ )
275
+ require.NoError(t, err)
276
+ require.True(t, ok)
277
+ assert.Equal(t, "svc.requests_total-instance=db1_eu-job=mysql_prod", route.chartID)
278
+}
279
+
280
+func TestBuildScalarAutogenRouteSanitizesLegacyLabelChars(t *testing.T) {
281
+ tests := []struct {
282
+ name string
283
+ value string
284
+ want string
285
+ }{
286
+ {name: "space", value: "a b", want: "a_b"},
287
+ {name: "backslash", value: "a\\b", want: "a_b"},
288
+ {name: "apostrophe", value: "a'b", want: "ab"},
289
+ }
290
+
291
+ for _, tc := range tests {
292
+ t.Run(tc.name, func(t *testing.T) {
293
+ route, ok, err := buildScalarAutogenRoute(
294
+ "svc.requests_total",
295
+ sortedLabelView(map[string]string{"instance": tc.value}),
296
+ metrix.SeriesMeta{Kind: metrix.MetricKindCounter},
297
+ AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
298
+ "",
299
+ )
300
+ require.NoError(t, err)
301
+ require.True(t, ok)
302
+ assert.Equal(t, "svc.requests_total-instance="+tc.want, route.chartID)
303
+ })
304
+ }
305
+}
src/go/plugin/framework/chartengine/identity.go
+10
-6
@@ -96,7 +96,7 @@ func renderInstanceSuffix(identity program.ChartIdentity, labels labelAccessor)
96
parts := make([]string, 0, len(values))
97
hasNonEmpty := false
98
for _, item := range values {
99
- part := sanitizeIDComponent(item.Value)
99
+ part := sanitizeChartIDLabelValue(item.Value)
100
if strings.TrimSpace(part) != "" {
101
hasNonEmpty = true
102
}
@@ -180,9 +180,13 @@ func resolveInstanceLabelValues(identity program.ChartIdentity, labels labelAcce
180
return out, true, nil
181
}
182
183
-func sanitizeIDComponent(value string) string {
184
- value = strings.ReplaceAll(value, " ", "_")
185
- value = strings.ReplaceAll(value, "\\", "_")
186
- value = strings.ReplaceAll(value, "'", "")
187
- return value
183
+var chartIDLabelValueSanitizer = strings.NewReplacer(
184
+ "\\", "_",
185
+ "'", "",
186
+ " ", "_",
187
+ ".", "_",
188
+)
189
+
190
+func sanitizeChartIDLabelValue(value string) string {
191
+ return chartIDLabelValueSanitizer.Replace(value)
192
}
src/go/plugin/framework/chartengine/identity_test.go
+72
@@ -104,3 +104,75 @@ func TestRenderChartInstanceIDScenarios(t *testing.T) {
104
})
105
}
106
}
107
+
108
+func TestRenderChartInstanceIDSanitizesDotLabelValues(t *testing.T) {
109
+ identity := program.ChartIdentity{
110
+ IDTemplate: program.Template{Raw: "win_nic_traffic"},
111
+ InstanceByLabels: []program.InstanceLabelSelector{
112
+ {Key: "nic"},
113
+ },
114
+ }
115
+
116
+ got, ok, err := renderChartInstanceID(identity, map[string]string{"nic": "eth0.100"})
117
+ require.NoError(t, err)
118
+ assert.True(t, ok)
119
+ assert.Equal(t, "win_nic_traffic_eth0_100", got)
120
+}
121
+
122
+func TestSanitizeChartIDLabelValue(t *testing.T) {
123
+ tests := map[string]struct {
124
+ value string
125
+ want string
126
+ }{
127
+ "empty": {
128
+ value: "",
129
+ want: "",
130
+ },
131
+ "no dots unchanged": {
132
+ value: "eth0",
133
+ want: "eth0",
134
+ },
135
+ "single dot replaced": {
136
+ value: "db1.eu",
137
+ want: "db1_eu",
138
+ },
139
+ "multiple dots replaced": {
140
+ value: "a.b.c",
141
+ want: "a_b_c",
142
+ },
143
+ }
144
+
145
+ for name, tc := range tests {
146
+ t.Run(name, func(t *testing.T) {
147
+ assert.Equal(t, tc.want, sanitizeChartIDLabelValue(tc.value))
148
+ })
149
+ }
150
+}
151
+
152
+func TestRenderChartInstanceIDSanitizesLegacyLabelChars(t *testing.T) {
153
+ identity := program.ChartIdentity{
154
+ IDTemplate: program.Template{Raw: "win_nic_traffic"},
155
+ InstanceByLabels: []program.InstanceLabelSelector{
156
+ {Key: "nic"},
157
+ },
158
+ }
159
+
160
+ tests := []struct {
161
+ name string
162
+ value string
163
+ want string
164
+ }{
165
+ {name: "space", value: "a b", want: "a_b"},
166
+ {name: "backslash", value: "a\\b", want: "a_b"},
167
+ {name: "apostrophe", value: "a'b", want: "ab"},
168
+ }
169
+
170
+ for _, tc := range tests {
171
+ t.Run(tc.name, func(t *testing.T) {
172
+ got, ok, err := renderChartInstanceID(identity, map[string]string{"nic": tc.value})
173
+ require.NoError(t, err)
174
+ assert.True(t, ok)
175
+ assert.Equal(t, "win_nic_traffic_"+tc.want, got)
176
+ })
177
+ }
178
+}