@cryptotaxi247 / netdata-1 / commits / c1bc28e77

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 +}