diff --git a/promql/parser/printer.go b/promql/parser/printer.go index 961167428b..67b13eaf12 100644 --- a/promql/parser/printer.go +++ b/promql/parser/printer.go @@ -109,7 +109,7 @@ func writeLabels(b *bytes.Buffer, ss []string) { if i > 0 { b.WriteString(", ") } - if !model.LegacyValidation.IsValidMetricName(s) { + if !model.LegacyValidation.IsValidLabelName(s) { b.Write(strconv.AppendQuote(b.AvailableBuffer(), s)) } else { b.WriteString(s) @@ -145,6 +145,19 @@ func (node *BinaryExpr) ShortString() string { return node.Op.String() + node.returnBool() + node.getMatchingStr() } +// joinLabels joins label names, quoting them if they are not valid legacy label names. +func joinLabels(labels []string) string { + quoted := make([]string, 0, len(labels)) + for _, label := range labels { + if model.LegacyValidation.IsValidLabelName(label) { + quoted = append(quoted, label) + } else { + quoted = append(quoted, strconv.Quote(label)) + } + } + return strings.Join(quoted, ", ") +} + func (node *BinaryExpr) getMatchingStr() string { matching := "" vm := node.VectorMatching @@ -154,7 +167,7 @@ func (node *BinaryExpr) getMatchingStr() string { if vm.On { vmTag = "on" } - matching = fmt.Sprintf(" %s (%s)", vmTag, strings.Join(vm.MatchingLabels, ", ")) + matching = fmt.Sprintf(" %s (%s)", vmTag, joinLabels(vm.MatchingLabels)) } if vm.Card == CardManyToOne || vm.Card == CardOneToMany { @@ -162,7 +175,7 @@ func (node *BinaryExpr) getMatchingStr() string { if vm.Card == CardManyToOne { vmCard = "left" } - matching += fmt.Sprintf(" group_%s (%s)", vmCard, strings.Join(vm.Include, ", ")) + matching += fmt.Sprintf(" group_%s (%s)", vmCard, joinLabels(vm.Include)) } } return matching diff --git a/promql/parser/printer_test.go b/promql/parser/printer_test.go index b28da988da..c9a3cb35e8 100644 --- a/promql/parser/printer_test.go +++ b/promql/parser/printer_test.go @@ -269,6 +269,10 @@ func TestExprString(t *testing.T) { { in: `predict_linear(foo[1h], 3000)`, }, + { + in: `sum by("üüü") (foo)`, + out: `sum by ("üüü") (foo)`, + }, } EnableExtendedRangeSelectors = true @@ -394,3 +398,45 @@ func TestVectorSelector_String(t *testing.T) { }) } } + +func TestBinaryExprUTF8Labels(t *testing.T) { + testCases := []struct { + name string + input string + expected string + }{ + { + name: "UTF-8 labels in on clause", + input: `foo / on("äää") bar`, + expected: `foo / on ("äää") bar`, + }, + { + name: "UTF-8 labels in group_left clause", + input: `foo / on("äää") group_left("ööö") bar`, + expected: `foo / on ("äää") group_left ("ööö") bar`, + }, + { + name: "Mixed legacy and UTF-8 labels", + input: `foo / on(legacy, "üüü") bar`, + expected: `foo / on (legacy, "üüü") bar`, + }, + { + name: "Legacy labels only (should not quote)", + input: `foo / on(job, instance) bar`, + expected: `foo / on (job, instance) bar`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expr, err := ParseExpr(tc.input) + if err != nil { + t.Fatalf("Failed to parse: %v", err) + } + result := expr.String() + if result != tc.expected { + t.Errorf("Expected: %s\nGot: %s", tc.expected, result) + } + }) + } +}