diff --git a/model/relabel/relabel.go b/model/relabel/relabel.go index 2bec6cfabb..1425141248 100644 --- a/model/relabel/relabel.go +++ b/model/relabel/relabel.go @@ -29,7 +29,8 @@ import ( ) var ( - relabelTarget = regexp.MustCompile(`^(?:(?:[a-zA-Z_]|\$(?:\{\w+\}|\w+))+\w*)+$`) + relabelTargetLegacy = regexp.MustCompile(`^(?:(?:[a-zA-Z_]|\$(?:\{\w+\}|\w+))+\w*)+$`) + relabelTargetUTF8 = regexp.MustCompile(`^(?:(?:[^\${}]|\$(?:\{[^\${}]+\}|[^\${}]+))+[^\${}]*)+$`) // All UTF-8 chars, but ${}. DefaultRelabelConfig = Config{ Action: Replace, @@ -124,10 +125,15 @@ func (c *Config) Validate() error { if (c.Action == Replace || c.Action == HashMod || c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && c.TargetLabel == "" { return fmt.Errorf("relabel configuration for %s action requires 'target_label' value", c.Action) } - if c.Action == Replace && !strings.Contains(c.TargetLabel, "$") && !model.LabelName(c.TargetLabel).IsValid() { + if c.Action == Replace && !varInRegexTemplate(c.TargetLabel) && !model.LabelName(c.TargetLabel).IsValid() { return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) } - if c.Action == Replace && strings.Contains(c.TargetLabel, "$") && !relabelTarget.MatchString(c.TargetLabel) { + + relabelTargetRe := relabelTargetUTF8 + if model.NameValidationScheme == model.LegacyValidation { + relabelTargetRe = relabelTargetLegacy + } + if c.Action == Replace && varInRegexTemplate(c.TargetLabel) && !relabelTargetRe.MatchString(c.TargetLabel) { return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) } if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && !model.LabelName(c.TargetLabel).IsValid() { @@ -136,7 +142,7 @@ func (c *Config) Validate() error { if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && c.Replacement != DefaultRelabelConfig.Replacement { return fmt.Errorf("'replacement' can not be set for %s action", c.Action) } - if c.Action == LabelMap && !relabelTarget.MatchString(c.Replacement) { + if c.Action == LabelMap && !relabelTargetRe.MatchString(c.Replacement) { return fmt.Errorf("%q is invalid 'replacement' for %s action", c.Replacement, c.Action) } if c.Action == HashMod && !model.LabelName(c.TargetLabel).IsValid() { diff --git a/model/relabel/relabel_test.go b/model/relabel/relabel_test.go index 6f234675c6..fbf8f05e07 100644 --- a/model/relabel/relabel_test.go +++ b/model/relabel/relabel_test.go @@ -593,6 +593,75 @@ func TestRelabel(t *testing.T) { "d": "match", }), }, + { // Replace on source label with UTF-8 characters. + input: labels.FromMap(map[string]string{ + "utf-8.label": "line1\nline2", + "b": "bar", + "c": "baz", + }), + relabel: []*Config{ + { + SourceLabels: model.LabelNames{"utf-8.label"}, + Regex: MustNewRegexp("line1.*line2"), + TargetLabel: "d", + Separator: ";", + Replacement: `match${1}`, + Action: Replace, + }, + }, + output: labels.FromMap(map[string]string{ + "utf-8.label": "line1\nline2", + "b": "bar", + "c": "baz", + "d": "match", + }), + }, + { // Replace targetLabel with UTF-8 characters. + input: labels.FromMap(map[string]string{ + "a": "line1\nline2", + "b": "bar", + "c": "baz", + }), + relabel: []*Config{ + { + SourceLabels: model.LabelNames{"a"}, + Regex: MustNewRegexp("line1.*line2"), + TargetLabel: "utf-8.label", + Separator: ";", + Replacement: `match${1}`, + Action: Replace, + }, + }, + output: labels.FromMap(map[string]string{ + "a": "line1\nline2", + "b": "bar", + "c": "baz", + "utf-8.label": "match", + }), + }, + { // Replace targetLabel with UTF-8 characters and $variable. + input: labels.FromMap(map[string]string{ + "a": "line1\nline2", + "b": "bar", + "c": "baz", + }), + relabel: []*Config{ + { + SourceLabels: model.LabelNames{"a"}, + Regex: MustNewRegexp("line1.*line2"), + TargetLabel: "utf-8.label${1}", + Separator: ";", + Replacement: `match${1}`, + Action: Replace, + }, + }, + output: labels.FromMap(map[string]string{ + "a": "line1\nline2", + "b": "bar", + "c": "baz", + "utf-8.label": "match", + }), + }, } for _, test := range tests { @@ -646,9 +715,8 @@ func TestRelabelValidate(t *testing.T) { config: Config{ Action: Lowercase, Replacement: DefaultRelabelConfig.Replacement, - TargetLabel: "${3}", + TargetLabel: "${3}", // With UTF-8 naming, this is now a legal relabel rule. }, - expected: `"${3}" is invalid 'target_label'`, }, { config: Config{ @@ -665,9 +733,8 @@ func TestRelabelValidate(t *testing.T) { Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), Action: Replace, Replacement: "${1}", - TargetLabel: "0${3}", + TargetLabel: "0${3}", // With UTF-8 naming this targets a valid label. }, - expected: `"0${3}" is invalid 'target_label'`, }, { config: Config{ @@ -675,9 +742,8 @@ func TestRelabelValidate(t *testing.T) { Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), Action: Replace, Replacement: "${1}", - TargetLabel: "-${3}", + TargetLabel: "-${3}", // With UTF-8 naming this targets a valid label. }, - expected: `"-${3}" is invalid 'target_label' for replace action`, }, } for i, test := range tests { @@ -693,30 +759,66 @@ func TestRelabelValidate(t *testing.T) { } func TestTargetLabelValidity(t *testing.T) { - tests := []struct { - str string - valid bool - }{ - {"-label", false}, - {"label", true}, - {"label${1}", true}, - {"${1}label", true}, - {"${1}", true}, - {"${1}label", true}, - {"${", false}, - {"$", false}, - {"${}", false}, - {"foo${", false}, - {"$1", true}, - {"asd$2asd", true}, - {"-foo${1}bar-", false}, - {"_${1}_", true}, - {"foo${bar}foo", true}, - } - for _, test := range tests { - require.Equal(t, test.valid, relabelTarget.Match([]byte(test.str)), - "Expected %q to be %v", test.str, test.valid) - } + t.Run("legacy", func(t *testing.T) { + for _, test := range []struct { + str string + valid bool + }{ + {"-label", false}, + {"label", true}, + {"label${1}", true}, + {"${1}label", true}, + {"${1}", true}, + {"${1}label", true}, + {"${", false}, + {"$", false}, + {"${}", false}, + {"foo${", false}, + {"$1", true}, + {"asd$2asd", true}, + {"-foo${1}bar-", false}, + {"bar.foo${1}bar", false}, + {"_${1}_", true}, + {"foo${bar}foo", true}, + } { + t.Run(test.str, func(t *testing.T) { + require.Equal(t, test.valid, relabelTargetLegacy.Match([]byte(test.str)), + "Expected %q to be %v", test.str, test.valid) + }) + } + }) + t.Run("utf-8", func(t *testing.T) { + for _, test := range []struct { + str string + valid bool + }{ + {"-label", true}, + {"label", true}, + {"label${1}", true}, + {"${1}label", true}, + {"${1}", true}, + {"${1}label", true}, + {"$1", true}, + {"asd$2asd", true}, + {"-foo${1}bar-", true}, + {"bar.foo${1}bar", true}, + {"_${1}_", true}, + {"foo${bar}foo", true}, + + // Those can be ambiguous. Currently, we assume user error. + {"${", false}, + {"$", false}, + {"${}", false}, + {"foo${", false}, + } { + t.Run(test.str, func(t *testing.T) { + require.Equal(t, test.valid, relabelTargetUTF8.Match([]byte(test.str)), + "Expected %q to be %v", test.str, test.valid) + }) + + } + }) + } func BenchmarkRelabel(b *testing.B) {