Skip to content

Commit 5aaf1ef

Browse files
victoriametrics: fix seriesByTag name→__graphite__ mapping and quote-aware tag splitting
Two fixes for seriesByTag conversion to PromQL: 1. VictoriaMetrics: replace fragile strings.ReplaceAll("'name=","'__name__=") with a proper tag rename via new SeriesByTagToPromQLWithRenames helper. The "name" tag (Graphite metric path) is now correctly mapped to __graphite__ (matching how VM stores Graphite data), not __name__. This is consistent with the non-seriesByTag path which already uses {__graphite__="..."}. 2. SplitTagValues: use quote-aware comma splitting so that tag values containing commas (e.g. seriesByTag('desc=hello, world')) are not incorrectly split into separate arguments. Includes tests for SplitTagValues and SeriesByTagToPromQLWithRenames. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent beaa7db commit 5aaf1ef

3 files changed

Lines changed: 188 additions & 9 deletions

File tree

zipper/protocols/prometheus/helpers/helpers.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func PromethizeTagValue(tagValue string) (string, types.Tag) {
160160
case idx+1 == len(tagValue): // != or = with empty value
161161
t.OP += "="
162162
case tagValue[idx+1] == '~':
163-
if len(t.OP) > 0 { // !=~
163+
if t.OP != "" { // !=~
164164
t.OP += "~"
165165
} else { // =~
166166
t.OP = "=~"
@@ -177,12 +177,37 @@ func PromethizeTagValue(tagValue string) (string, types.Tag) {
177177
return tagName, t
178178
}
179179

180-
// SplitTagValues - For given tag-value list converts it to more usable map[string]Tag, where string is TagName
180+
// splitWithQuotes splits s by delimiter, but skips delimiters inside single- or double-quoted regions.
181+
// This is needed because seriesByTag arguments are quoted ('tag=value') and tag values themselves may
182+
// contain commas which must not be treated as argument separators.
183+
func splitWithQuotes(s string, delimiter rune) []string {
184+
var result []string
185+
start := 0
186+
var quoteChar rune // 0 = outside quotes
187+
for i, c := range s {
188+
switch {
189+
case c == quoteChar:
190+
quoteChar = 0
191+
case quoteChar == 0 && (c == '\'' || c == '"'):
192+
quoteChar = c
193+
case quoteChar == 0 && c == delimiter:
194+
result = append(result, s[start:i])
195+
start = i + 1
196+
}
197+
}
198+
return append(result, s[start:])
199+
}
200+
201+
// SplitTagValues - For given tag-value list converts it to more usable map[string]Tag, where string is TagName.
202+
// Splits by comma but respects commas inside quoted strings (e.g. seriesByTag('tag=value','other=val,ue')).
181203
func SplitTagValues(query string) map[string]types.Tag {
182-
tags := strings.Split(query, ",")
204+
tags := splitWithQuotes(query, ',')
183205
result := make(map[string]types.Tag)
184206
for _, tvString := range tags {
185207
tvString = strings.TrimSpace(tvString)
208+
if len(tvString) < 2 {
209+
continue
210+
}
186211
name, tag := PromethizeTagValue(tvString[1 : len(tvString)-1])
187212
result[name] = tag
188213
}
@@ -210,13 +235,37 @@ func PromMetricToGraphite(metric map[string]string) string {
210235
return res.String()
211236
}
212237

213-
// SeriesByTagToPromQL converts graphite SeriesByTag to PromQL
214-
// will return step if __step__ is passed
238+
// SeriesByTagToPromQL converts graphite SeriesByTag to PromQL.
239+
// Returns step (possibly overridden by __step__ tag) and the PromQL selector string.
215240
func SeriesByTagToPromQL(step, target string) (string, string) {
241+
return convertSeriesByTagToPromQL(step, target, nil)
242+
}
243+
244+
// SeriesByTagToPromQLWithRenames is like SeriesByTagToPromQL but applies tagRenames
245+
// to tag names after parsing and before building the PromQL selector. For example,
246+
// passing map[string]string{"name": "__graphite__"} renames the Graphite "name" tag
247+
// to the VictoriaMetrics "__graphite__" label.
248+
//
249+
// Note: if both the old and new tag names exist in the query, the renamed tag
250+
// overwrites the existing one (last-write-wins).
251+
func SeriesByTagToPromQLWithRenames(step, target string, tagRenames map[string]string) (string, string) {
252+
return convertSeriesByTagToPromQL(step, target, tagRenames)
253+
}
254+
255+
func convertSeriesByTagToPromQL(step, target string, tagRenames map[string]string) (string, string) {
216256
firstTag := true
217257
var queryBuilder strings.Builder
218258
tagsString := target[len("seriesByTag(") : len(target)-1]
219259
tvs := SplitTagValues(tagsString)
260+
261+
// Apply tag renames (e.g. "name" -> "__graphite__" for VictoriaMetrics).
262+
for oldName, newName := range tagRenames {
263+
if v, ok := tvs[oldName]; ok {
264+
delete(tvs, oldName)
265+
tvs[newName] = v
266+
}
267+
}
268+
220269
// It's ok to have empty "__name__"
221270
if v, ok := tvs["__name__"]; ok {
222271
if v.OP == "=" {
@@ -229,7 +278,15 @@ func SeriesByTagToPromQL(step, target string) (string, string) {
229278

230279
delete(tvs, "__name__")
231280
}
232-
for tagName, t := range tvs {
281+
// Sort tag names to produce deterministic PromQL output.
282+
tagNames := make([]string, 0, len(tvs))
283+
for tagName := range tvs {
284+
tagNames = append(tagNames, tagName)
285+
}
286+
sort.Strings(tagNames)
287+
288+
for _, tagName := range tagNames {
289+
t := tvs[tagName]
233290
if tagName == "__step__" {
234291
step = t.TagValue
235292
continue

zipper/protocols/prometheus/helpers/helpers_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,122 @@ func TestPromethizeTagValue(t *testing.T) {
250250
}
251251

252252
}
253+
254+
func TestSplitTagValues(t *testing.T) {
255+
tests := []struct {
256+
name string
257+
query string
258+
want map[string]types.Tag
259+
}{
260+
{
261+
name: "simple two tags",
262+
query: "'env=prod','host=web1'",
263+
want: map[string]types.Tag{
264+
"env": {OP: "=", TagValue: "prod"},
265+
"host": {OP: "=", TagValue: "web1"},
266+
},
267+
},
268+
{
269+
name: "comma inside quoted value",
270+
query: "'env=prod','desc=hello, world'",
271+
want: map[string]types.Tag{
272+
"env": {OP: "=", TagValue: "prod"},
273+
"desc": {OP: "=", TagValue: "hello, world"},
274+
},
275+
},
276+
{
277+
name: "empty input",
278+
query: "",
279+
want: map[string]types.Tag{},
280+
},
281+
{
282+
name: "single tag",
283+
query: "'name=cpu.load'",
284+
want: map[string]types.Tag{
285+
"name": {OP: "=", TagValue: "cpu.load"},
286+
},
287+
},
288+
{
289+
name: "trailing comma produces empty segment (skipped)",
290+
query: "'env=prod',",
291+
want: map[string]types.Tag{
292+
"env": {OP: "=", TagValue: "prod"},
293+
},
294+
},
295+
{
296+
name: "whitespace around tags",
297+
query: " 'env=prod' , 'host=web1' ",
298+
want: map[string]types.Tag{
299+
"env": {OP: "=", TagValue: "prod"},
300+
"host": {OP: "=", TagValue: "web1"},
301+
},
302+
},
303+
{
304+
name: "double-quoted values",
305+
query: `"env=prod","host=web1"`,
306+
want: map[string]types.Tag{
307+
"env": {OP: "=", TagValue: "prod"},
308+
"host": {OP: "=", TagValue: "web1"},
309+
},
310+
},
311+
}
312+
313+
for _, tt := range tests {
314+
t.Run(tt.name, func(t *testing.T) {
315+
got := SplitTagValues(tt.query)
316+
assert.Equal(t, tt.want, got)
317+
})
318+
}
319+
}
320+
321+
func TestSeriesByTagToPromQLWithRenames(t *testing.T) {
322+
tests := []struct {
323+
name string
324+
step string
325+
target string
326+
renames map[string]string
327+
wantStep string
328+
wantPromQL string
329+
}{
330+
{
331+
name: "VM: name renamed to __graphite__",
332+
step: "30",
333+
target: "seriesByTag('name=servers.web.cpu','env=prod')",
334+
renames: map[string]string{"name": "__graphite__"},
335+
wantStep: "30",
336+
wantPromQL: `{__graphite__="servers.web.cpu", env="prod"}`,
337+
},
338+
{
339+
name: "VM: hostname not affected by name rename",
340+
step: "30",
341+
target: "seriesByTag('hostname=web1','env=prod')",
342+
renames: map[string]string{"name": "__graphite__"},
343+
wantStep: "30",
344+
wantPromQL: `{env="prod", hostname="web1"}`,
345+
},
346+
{
347+
name: "no renames, same as SeriesByTagToPromQL",
348+
step: "60",
349+
target: "seriesByTag('__name__=~cpu.*','env=prod')",
350+
renames: nil,
351+
wantStep: "60",
352+
wantPromQL: `{__name__=~"cpu.*", env="prod"}`,
353+
},
354+
{
355+
name: "rename collision: existing tag overwritten",
356+
step: "30",
357+
target: "seriesByTag('name=metric.path','__graphite__=old')",
358+
renames: map[string]string{"name": "__graphite__"},
359+
wantStep: "30",
360+
wantPromQL: `{__graphite__="metric.path"}`,
361+
},
362+
}
363+
364+
for _, tt := range tests {
365+
t.Run(tt.name, func(t *testing.T) {
366+
gotStep, gotPromQL := SeriesByTagToPromQLWithRenames(tt.step, tt.target, tt.renames)
367+
assert.Equal(t, tt.wantStep, gotStep)
368+
assert.Equal(t, tt.wantPromQL, gotPromQL)
369+
})
370+
}
371+
}

zipper/protocols/victoriametrics/fetch.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func (c *VictoriaMetricsGroup) Fetch(ctx context.Context, request *protov3.Multi
3333
logger := c.logger.With(zap.String("type", "fetch"), zap.String("request", request.String()))
3434
stats := &types.Stats{}
3535
var serverUrl string
36-
if len(c.vmClusterTenantID) > 0 {
36+
if c.vmClusterTenantID != "" {
3737
serverUrl = fmt.Sprintf("http://127.0.0.1/select/%s/prometheus/api/v1/query_range", c.vmClusterTenantID)
3838
} else {
3939
serverUrl = "http://127.0.0.1/api/v1/query_range"
@@ -75,8 +75,11 @@ func (c *VictoriaMetricsGroup) Fetch(ctx context.Context, request *protov3.Multi
7575
// Make local copy
7676
stepLocalStr := target.step
7777
if strings.HasPrefix(target.name, "seriesByTag") {
78-
target.name = strings.ReplaceAll(target.name, "'name=", "'__name__=")
79-
stepLocalStr, target.name = helpers.SeriesByTagToPromQL(stepLocalStr, target.name)
78+
// VictoriaMetrics stores Graphite metric paths in the __graphite__ label,
79+
// so rename the Graphite "name" tag accordingly before building PromQL.
80+
stepLocalStr, target.name = helpers.SeriesByTagToPromQLWithRenames(stepLocalStr, target.name, map[string]string{
81+
"name": "__graphite__",
82+
})
8083
} else {
8184
target.name = fmt.Sprintf("{__graphite__=%q}", target.name)
8285
}

0 commit comments

Comments
 (0)