From 2d3be80d545b5e5f9a12564ed7dfd296e9fd8a43 Mon Sep 17 00:00:00 2001 From: Julius Hinze Date: Fri, 25 Jul 2025 14:32:59 +0200 Subject: [PATCH] Parameterize metric/label name validation scheme Parameterized metric/label name validation scheme Signed-off-by: Arve Knudsen --- cmd/prometheus/main.go | 5 +- cmd/promtool/main.go | 43 ++-- cmd/promtool/main_test.go | 24 +- cmd/promtool/rules.go | 21 +- cmd/promtool/rules_test.go | 11 +- cmd/promtool/sd_test.go | 11 +- cmd/promtool/unittest.go | 11 +- config/config.go | 68 +++++- config/config_test.go | 198 +++++++++------- go.mod | 2 +- go.sum | 4 +- model/labels/labels_common.go | 13 +- model/relabel/relabel.go | 32 ++- model/relabel/relabel_test.go | 64 ++++-- model/rulefmt/rulefmt.go | 29 ++- model/rulefmt/rulefmt_test.go | 30 ++- .../legacy_validation_annotation.bad.yaml | 7 + .../legacy_validation_annotation.good.yaml | 7 + model/textparse/protobufparse.go | 2 +- notifier/manager.go | 18 +- notifier/manager_test.go | 65 +++--- prompb/io/prometheus/client/decoder.go | 6 +- promql/engine.go | 2 +- promql/functions.go | 6 +- promql/parser/generated_parser.y | 4 +- promql/parser/generated_parser.y.go | 4 +- promql/parser/printer.go | 2 +- rules/alerting_test.go | 13 +- rules/group.go | 4 +- rules/manager.go | 14 +- rules/manager_test.go | 215 ++++++++++-------- scrape/manager_test.go | 26 ++- scrape/scrape.go | 10 + scrape/scrape_test.go | 119 +++++----- storage/remote/codec.go | 4 +- storage/remote/queue_manager_test.go | 13 +- storage/remote/write_test.go | 8 +- web/api/v1/api.go | 3 +- web/api/v1/api_test.go | 11 +- 39 files changed, 685 insertions(+), 444 deletions(-) create mode 100644 model/rulefmt/testdata/legacy_validation_annotation.bad.yaml create mode 100644 model/rulefmt/testdata/legacy_validation_annotation.good.yaml diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index c4e3fe7914..db36630f94 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -651,7 +651,7 @@ func main() { } // Parse rule files to verify they exist and contain valid rules. - if err := rules.ParseFiles(cfgFile.RuleFiles); err != nil { + if err := rules.ParseFiles(cfgFile.RuleFiles, cfgFile.GlobalConfig.MetricNameValidationScheme); err != nil { absPath, pathErr := filepath.Abs(cfg.configFile) if pathErr != nil { absPath = cfg.configFile @@ -790,7 +790,7 @@ func main() { ctxWeb, cancelWeb = context.WithCancel(context.Background()) ctxRule = context.Background() - notifierManager = notifier.NewManager(&cfg.notifier, logger.With("component", "notifier")) + notifierManager = notifier.NewManager(&cfg.notifier, cfgFile.GlobalConfig.MetricNameValidationScheme, logger.With("component", "notifier")) ctxScrape, cancelScrape = context.WithCancel(context.Background()) ctxNotify, cancelNotify = context.WithCancel(context.Background()) @@ -867,6 +867,7 @@ func main() { queryEngine = promql.NewEngine(opts) ruleManager = rules.NewManager(&rules.ManagerOptions{ + NameValidationScheme: cfgFile.GlobalConfig.MetricNameValidationScheme, Appendable: fanoutStorage, Queryable: localStorage, QueryFunc: rules.EngineQueryFunc(queryEngine, fanoutStorage), diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index 7b5b6a603a..dadf480b94 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -359,7 +359,7 @@ func main() { os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, prometheus.DefaultRegisterer)) case checkConfigCmd.FullCommand(): - os.Exit(CheckConfig(*agentMode, *checkConfigSyntaxOnly, newConfigLintConfig(*checkConfigLint, *checkConfigLintFatal, *checkConfigIgnoreUnknownFields, model.Duration(*checkLookbackDelta)), *configFiles...)) + os.Exit(CheckConfig(*agentMode, *checkConfigSyntaxOnly, newConfigLintConfig(*checkConfigLint, *checkConfigLintFatal, *checkConfigIgnoreUnknownFields, model.UTF8Validation, model.Duration(*checkLookbackDelta)), *configFiles...)) case checkServerHealthCmd.FullCommand(): os.Exit(checkErr(CheckServerStatus(serverURL, checkHealth, httpRoundTripper))) @@ -371,7 +371,7 @@ func main() { os.Exit(CheckWebConfig(*webConfigFiles...)) case checkRulesCmd.FullCommand(): - os.Exit(CheckRules(newRulesLintConfig(*checkRulesLint, *checkRulesLintFatal, *checkRulesIgnoreUnknownFields), *ruleFiles...)) + os.Exit(CheckRules(newRulesLintConfig(*checkRulesLint, *checkRulesLintFatal, *checkRulesIgnoreUnknownFields, model.UTF8Validation), *ruleFiles...)) case checkMetricsCmd.FullCommand(): os.Exit(CheckMetrics(*checkMetricsExtended)) @@ -436,7 +436,7 @@ func main() { os.Exit(backfillOpenMetrics(*importFilePath, *importDBPath, *importHumanReadable, *importQuiet, *maxBlockDuration, *openMetricsLabels)) case importRulesCmd.FullCommand(): - os.Exit(checkErr(importRules(serverURL, httpRoundTripper, *importRulesStart, *importRulesEnd, *importRulesOutputDir, *importRulesEvalInterval, *maxBlockDuration, *importRulesFiles...))) + os.Exit(checkErr(importRules(serverURL, httpRoundTripper, *importRulesStart, *importRulesEnd, *importRulesOutputDir, *importRulesEvalInterval, *maxBlockDuration, model.UTF8Validation, *importRulesFiles...))) case queryAnalyzeCmd.FullCommand(): os.Exit(checkErr(queryAnalyzeCfg.run(serverURL, httpRoundTripper))) @@ -468,17 +468,19 @@ func checkExperimental(f bool) { var errLint = errors.New("lint error") type rulesLintConfig struct { - all bool - duplicateRules bool - fatal bool - ignoreUnknownFields bool + all bool + duplicateRules bool + fatal bool + ignoreUnknownFields bool + nameValidationScheme model.ValidationScheme } -func newRulesLintConfig(stringVal string, fatal, ignoreUnknownFields bool) rulesLintConfig { +func newRulesLintConfig(stringVal string, fatal, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) rulesLintConfig { items := strings.Split(stringVal, ",") ls := rulesLintConfig{ - fatal: fatal, - ignoreUnknownFields: ignoreUnknownFields, + fatal: fatal, + ignoreUnknownFields: ignoreUnknownFields, + nameValidationScheme: nameValidationScheme, } for _, setting := range items { switch setting { @@ -504,7 +506,7 @@ type configLintConfig struct { lookbackDelta model.Duration } -func newConfigLintConfig(optionsStr string, fatal, ignoreUnknownFields bool, lookbackDelta model.Duration) configLintConfig { +func newConfigLintConfig(optionsStr string, fatal, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme, lookbackDelta model.Duration) configLintConfig { c := configLintConfig{ rulesLintConfig: rulesLintConfig{ fatal: fatal, @@ -533,7 +535,7 @@ func newConfigLintConfig(optionsStr string, fatal, ignoreUnknownFields bool, loo } if len(rulesOptions) > 0 { - c.rulesLintConfig = newRulesLintConfig(strings.Join(rulesOptions, ","), fatal, ignoreUnknownFields) + c.rulesLintConfig = newRulesLintConfig(strings.Join(rulesOptions, ","), fatal, ignoreUnknownFields, nameValidationScheme) } return c @@ -854,7 +856,7 @@ func checkRulesFromStdin(ls rulesLintConfig) (bool, bool) { fmt.Fprintln(os.Stderr, " FAILED:", err) return true, true } - rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields) + rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields, ls.nameValidationScheme) if errs != nil { failed = true fmt.Fprintln(os.Stderr, " FAILED:") @@ -888,7 +890,7 @@ func checkRules(files []string, ls rulesLintConfig) (bool, bool) { hasErrors := false for _, f := range files { fmt.Println("Checking", f) - rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields) + rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields, ls.nameValidationScheme) if errs != nil { failed = true fmt.Fprintln(os.Stderr, " FAILED:") @@ -1225,7 +1227,7 @@ func (j *jsonPrinter) printLabelValues(v model.LabelValues) { // importRules backfills recording rules from the files provided. The output are blocks of data // at the outputDir location. -func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outputDir string, evalInterval, maxBlockDuration time.Duration, files ...string) error { +func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outputDir string, evalInterval, maxBlockDuration time.Duration, nameValidationScheme model.ValidationScheme, files ...string) error { ctx := context.Background() var stime, etime time.Time var err error @@ -1248,11 +1250,12 @@ func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outpu } cfg := ruleImporterConfig{ - outputDir: outputDir, - start: stime, - end: etime, - evalInterval: evalInterval, - maxBlockDuration: maxBlockDuration, + outputDir: outputDir, + start: stime, + end: etime, + evalInterval: evalInterval, + maxBlockDuration: maxBlockDuration, + nameValidationScheme: nameValidationScheme, } api, err := newAPI(url, roundTripper, nil) if err != nil { diff --git a/cmd/promtool/main_test.go b/cmd/promtool/main_test.go index f922d18c4e..17acb5ad02 100644 --- a/cmd/promtool/main_test.go +++ b/cmd/promtool/main_test.go @@ -186,7 +186,7 @@ func TestCheckDuplicates(t *testing.T) { c := test t.Run(c.name, func(t *testing.T) { t.Parallel() - rgs, err := rulefmt.ParseFile(c.ruleFile, false) + rgs, err := rulefmt.ParseFile(c.ruleFile, false, model.UTF8Validation) require.Empty(t, err) dups := checkDuplicates(rgs.Groups) require.Equal(t, c.expectedDups, dups) @@ -195,7 +195,7 @@ func TestCheckDuplicates(t *testing.T) { } func BenchmarkCheckDuplicates(b *testing.B) { - rgs, err := rulefmt.ParseFile("./testdata/rules_large.yml", false) + rgs, err := rulefmt.ParseFile("./testdata/rules_large.yml", false, model.UTF8Validation) require.Empty(b, err) b.ResetTimer() @@ -509,7 +509,7 @@ func TestCheckRules(t *testing.T) { defer func(v *os.File) { os.Stdin = v }(os.Stdin) os.Stdin = r - exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false)) + exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation)) require.Equal(t, successExitCode, exitCode) }) @@ -531,7 +531,7 @@ func TestCheckRules(t *testing.T) { defer func(v *os.File) { os.Stdin = v }(os.Stdin) os.Stdin = r - exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false)) + exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation)) require.Equal(t, failureExitCode, exitCode) }) @@ -553,7 +553,7 @@ func TestCheckRules(t *testing.T) { defer func(v *os.File) { os.Stdin = v }(os.Stdin) os.Stdin = r - exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false)) + exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation)) require.Equal(t, lintErrExitCode, exitCode) }) } @@ -571,19 +571,19 @@ func TestCheckRulesWithFeatureFlag(t *testing.T) { func TestCheckRulesWithRuleFiles(t *testing.T) { t.Run("rules-good", func(t *testing.T) { t.Parallel() - exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false), "./testdata/rules.yml") + exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), "./testdata/rules.yml") require.Equal(t, successExitCode, exitCode) }) t.Run("rules-bad", func(t *testing.T) { t.Parallel() - exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false), "./testdata/rules-bad.yml") + exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation), "./testdata/rules-bad.yml") require.Equal(t, failureExitCode, exitCode) }) t.Run("rules-lint-fatal", func(t *testing.T) { t.Parallel() - exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false), "./testdata/prometheus-rules.lint.yml") + exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation), "./testdata/prometheus-rules.lint.yml") require.Equal(t, lintErrExitCode, exitCode) }) } @@ -612,20 +612,20 @@ func TestCheckScrapeConfigs(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { // Non-fatal linting. - code := CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, false, false, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") + code := CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, false, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") require.Equal(t, successExitCode, code, "Non-fatal linting should return success") // Fatal linting. - code = CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") + code = CheckConfig(false, false, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") if tc.expectError { require.Equal(t, lintErrExitCode, code, "Fatal linting should return error") } else { require.Equal(t, successExitCode, code, "Fatal linting should return success when there are no problems") } // Check syntax only, no linting. - code = CheckConfig(false, true, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") + code = CheckConfig(false, true, newConfigLintConfig(lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") require.Equal(t, successExitCode, code, "Fatal linting should return success when checking syntax only") // Lint option "none" should disable linting. - code = CheckConfig(false, false, newConfigLintConfig(lintOptionNone+","+lintOptionTooLongScrapeInterval, true, false, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") + code = CheckConfig(false, false, newConfigLintConfig(lintOptionNone+","+lintOptionTooLongScrapeInterval, true, false, model.UTF8Validation, tc.lookbackDelta), "./testdata/prometheus-config.lint.too_long_scrape_interval.yml") require.Equal(t, successExitCode, code, `Fatal linting should return success when lint option "none" is specified`) }) } diff --git a/cmd/promtool/rules.go b/cmd/promtool/rules.go index b2eb18ca8e..98f2c38b58 100644 --- a/cmd/promtool/rules.go +++ b/cmd/promtool/rules.go @@ -48,11 +48,12 @@ type ruleImporter struct { } type ruleImporterConfig struct { - outputDir string - start time.Time - end time.Time - evalInterval time.Duration - maxBlockDuration time.Duration + outputDir string + start time.Time + end time.Time + evalInterval time.Duration + maxBlockDuration time.Duration + nameValidationScheme model.ValidationScheme } // newRuleImporter creates a new rule importer that can be used to parse and evaluate recording rule files and create new series @@ -60,10 +61,12 @@ type ruleImporterConfig struct { func newRuleImporter(logger *slog.Logger, config ruleImporterConfig, apiClient queryRangeAPI) *ruleImporter { logger.Info("new rule importer", "component", "backfiller", "start", config.start.Format(time.RFC822), "end", config.end.Format(time.RFC822)) return &ruleImporter{ - logger: logger, - config: config, - apiClient: apiClient, - ruleManager: rules.NewManager(&rules.ManagerOptions{}), + logger: logger, + config: config, + apiClient: apiClient, + ruleManager: rules.NewManager(&rules.ManagerOptions{ + NameValidationScheme: config.nameValidationScheme, + }), } } diff --git a/cmd/promtool/rules_test.go b/cmd/promtool/rules_test.go index 3cb47aa8af..5bbd61dd09 100644 --- a/cmd/promtool/rules_test.go +++ b/cmd/promtool/rules_test.go @@ -165,11 +165,12 @@ func TestBackfillRuleIntegration(t *testing.T) { func newTestRuleImporter(_ context.Context, start time.Time, tmpDir string, testSamples model.Matrix, maxBlockDuration time.Duration) (*ruleImporter, error) { logger := promslog.NewNopLogger() cfg := ruleImporterConfig{ - outputDir: tmpDir, - start: start.Add(-10 * time.Hour), - end: start.Add(-7 * time.Hour), - evalInterval: 60 * time.Second, - maxBlockDuration: maxBlockDuration, + outputDir: tmpDir, + start: start.Add(-10 * time.Hour), + end: start.Add(-7 * time.Hour), + evalInterval: 60 * time.Second, + maxBlockDuration: maxBlockDuration, + nameValidationScheme: model.UTF8Validation, } return newRuleImporter(logger, cfg, mockQueryRangeAPI{ diff --git a/cmd/promtool/sd_test.go b/cmd/promtool/sd_test.go index 8f174a9b80..e41c9893b2 100644 --- a/cmd/promtool/sd_test.go +++ b/cmd/promtool/sd_test.go @@ -42,11 +42,12 @@ func TestSDCheckResult(t *testing.T) { ScrapeInterval: model.Duration(1 * time.Minute), ScrapeTimeout: model.Duration(10 * time.Second), RelabelConfigs: []*relabel.Config{{ - SourceLabels: model.LabelNames{"foo"}, - Action: relabel.Replace, - TargetLabel: "newfoo", - Regex: reg, - Replacement: "$1", + SourceLabels: model.LabelNames{"foo"}, + Action: relabel.Replace, + TargetLabel: "newfoo", + Regex: reg, + Replacement: "$1", + NameValidationScheme: model.UTF8Validation, }}, } diff --git a/cmd/promtool/unittest.go b/cmd/promtool/unittest.go index 4910a0b1a6..4094bbbb32 100644 --- a/cmd/promtool/unittest.go +++ b/cmd/promtool/unittest.go @@ -222,11 +222,12 @@ func (tg *testGroup) test(testname string, evalInterval time.Duration, groupOrde // Load the rule files. opts := &rules.ManagerOptions{ - QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()), - Appendable: suite.Storage(), - Context: context.Background(), - NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {}, - Logger: promslog.NewNopLogger(), + QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()), + Appendable: suite.Storage(), + Context: context.Background(), + NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {}, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } m := rules.NewManager(opts) groupsMap, ers := m.LoadGroups(time.Duration(tg.Interval), tg.ExternalLabels, tg.ExternalURL, nil, ignoreUnknownFields, ruleFiles...) diff --git a/config/config.go b/config/config.go index 64dae3e8ac..058b4bf881 100644 --- a/config/config.go +++ b/config/config.go @@ -413,6 +413,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { jobNames[scfg.JobName] = struct{}{} } + if err := c.AlertingConfig.Validate(c.GlobalConfig.MetricNameValidationScheme); err != nil { + return err + } + rwNames := map[string]struct{}{} for _, rwcfg := range c.RemoteWriteConfigs { if rwcfg == nil { @@ -422,6 +426,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if _, ok := rwNames[rwcfg.Name]; ok && rwcfg.Name != "" { return fmt.Errorf("found multiple remote write configs with job name %q", rwcfg.Name) } + if err := rwcfg.Validate(c.GlobalConfig.MetricNameValidationScheme); err != nil { + return err + } rwNames[rwcfg.Name] = struct{}{} } rrNames := map[string]struct{}{} @@ -596,8 +603,14 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } + switch gc.MetricNameValidationScheme { + case model.UTF8Validation, model.LegacyValidation: + default: + gc.MetricNameValidationScheme = DefaultGlobalConfig.MetricNameValidationScheme + } + if err := gc.ExternalLabels.Validate(func(l labels.Label) error { - if !model.LabelName(l.Name).IsValid() { + if !gc.MetricNameValidationScheme.IsValidLabelName(l.Name) { return fmt.Errorf("%q is not a valid label name", l.Name) } if !model.LabelValue(l.Value).IsValid() { @@ -878,11 +891,9 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { } switch globalConfig.MetricNameValidationScheme { - case model.UnsetValidation: - globalConfig.MetricNameValidationScheme = model.UTF8Validation case model.LegacyValidation, model.UTF8Validation: default: - return fmt.Errorf("unknown global name validation method specified, must be either '', 'legacy' or 'utf8', got %s", globalConfig.MetricNameValidationScheme) + return errors.New("global name validation method must be set") } // Scrapeconfig validation scheme matches global if left blank. localValidationUnset := false @@ -944,6 +955,17 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { c.AlwaysScrapeClassicHistograms = &global } + for _, rc := range c.RelabelConfigs { + if err := rc.Validate(c.MetricNameValidationScheme); err != nil { + return err + } + } + for _, rc := range c.MetricRelabelConfigs { + if err := rc.Validate(c.MetricNameValidationScheme); err != nil { + return err + } + } + return nil } @@ -1096,6 +1118,20 @@ type AlertingConfig struct { AlertmanagerConfigs AlertmanagerConfigs `yaml:"alertmanagers,omitempty"` } +func (c *AlertingConfig) Validate(nameValidationScheme model.ValidationScheme) error { + for _, rc := range c.AlertRelabelConfigs { + if err := rc.Validate(nameValidationScheme); err != nil { + return err + } + } + for _, rc := range c.AlertmanagerConfigs { + if err := rc.Validate(nameValidationScheme); err != nil { + return err + } + } + return nil +} + // SetDirectory joins any relative file paths with dir. func (c *AlertingConfig) SetDirectory(dir string) { for _, c := range c.AlertmanagerConfigs { @@ -1240,6 +1276,20 @@ func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) er return nil } +func (c *AlertmanagerConfig) Validate(nameValidationScheme model.ValidationScheme) error { + for _, rc := range c.AlertRelabelConfigs { + if err := rc.Validate(nameValidationScheme); err != nil { + return err + } + } + for _, rc := range c.RelabelConfigs { + if err := rc.Validate(nameValidationScheme); err != nil { + return err + } + } + return nil +} + // MarshalYAML implements the yaml.Marshaler interface. func (c *AlertmanagerConfig) MarshalYAML() (interface{}, error) { return discovery.MarshalYAMLWithInlineConfigs(c) @@ -1377,6 +1427,16 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err return validateAuthConfigs(c) } +func (c *RemoteWriteConfig) Validate(nameValidationScheme model.ValidationScheme) error { + for _, rc := range c.WriteRelabelConfigs { + if err := rc.Validate(nameValidationScheme); err != nil { + return err + } + } + + return nil +} + // validateAuthConfigs validates that at most one of basic_auth, authorization, oauth2, sigv4, azuread or google_iam must be configured. func validateAuthConfigs(c *RemoteWriteConfig) error { var authConfigured []string diff --git a/config/config_test.go b/config/config_test.go index 527d3cd319..971fdb85f5 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -23,6 +23,8 @@ import ( "time" "github.com/alecthomas/units" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/regexp" "github.com/prometheus/common/config" "github.com/prometheus/common/model" @@ -106,6 +108,7 @@ var expectedConf = &Config{ ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols, AlwaysScrapeClassicHistograms: false, ConvertClassicHistogramsToNHCB: false, + MetricNameValidationScheme: model.UTF8Validation, }, Runtime: RuntimeConfig{ @@ -125,11 +128,12 @@ var expectedConf = &Config{ Name: "drop_expensive", WriteRelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"__name__"}, - Separator: ";", - Regex: relabel.MustNewRegexp("expensive.*"), - Replacement: "$1", - Action: relabel.Drop, + SourceLabels: model.LabelNames{"__name__"}, + Separator: ";", + Regex: relabel.MustNewRegexp("expensive.*"), + Replacement: "$1", + Action: relabel.Drop, + NameValidationScheme: model.UTF8Validation, }, }, QueueConfig: DefaultQueueConfig, @@ -279,50 +283,56 @@ var expectedConf = &Config{ RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"job", "__meta_dns_name"}, - TargetLabel: "job", - Separator: ";", - Regex: relabel.MustNewRegexp("(.*)some-[regex]"), - Replacement: "foo-${1}", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"job", "__meta_dns_name"}, + TargetLabel: "job", + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)some-[regex]"), + Replacement: "foo-${1}", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"abc"}, - TargetLabel: "cde", - Separator: ";", - Regex: relabel.DefaultRelabelConfig.Regex, - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.Replace, + SourceLabels: model.LabelNames{"abc"}, + TargetLabel: "cde", + Separator: ";", + Regex: relabel.DefaultRelabelConfig.Regex, + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - TargetLabel: "abc", - Separator: ";", - Regex: relabel.DefaultRelabelConfig.Regex, - Replacement: "static", - Action: relabel.Replace, + TargetLabel: "abc", + Separator: ";", + Regex: relabel.DefaultRelabelConfig.Regex, + Replacement: "static", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - TargetLabel: "abc", - Separator: ";", - Regex: relabel.MustNewRegexp(""), - Replacement: "static", - Action: relabel.Replace, + TargetLabel: "abc", + Separator: ";", + Regex: relabel.MustNewRegexp(""), + Replacement: "static", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"foo"}, - TargetLabel: "abc", - Action: relabel.KeepEqual, - Regex: relabel.DefaultRelabelConfig.Regex, - Replacement: relabel.DefaultRelabelConfig.Replacement, - Separator: relabel.DefaultRelabelConfig.Separator, + SourceLabels: model.LabelNames{"foo"}, + TargetLabel: "abc", + Action: relabel.KeepEqual, + Regex: relabel.DefaultRelabelConfig.Regex, + Replacement: relabel.DefaultRelabelConfig.Replacement, + Separator: relabel.DefaultRelabelConfig.Separator, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"foo"}, - TargetLabel: "abc", - Action: relabel.DropEqual, - Regex: relabel.DefaultRelabelConfig.Regex, - Replacement: relabel.DefaultRelabelConfig.Replacement, - Separator: relabel.DefaultRelabelConfig.Separator, + SourceLabels: model.LabelNames{"foo"}, + TargetLabel: "abc", + Action: relabel.DropEqual, + Regex: relabel.DefaultRelabelConfig.Regex, + Replacement: relabel.DefaultRelabelConfig.Replacement, + Separator: relabel.DefaultRelabelConfig.Separator, + NameValidationScheme: model.UTF8Validation, }, }, }, @@ -377,54 +387,61 @@ var expectedConf = &Config{ RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"job"}, - Regex: relabel.MustNewRegexp("(.*)some-[regex]"), - Separator: ";", - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.Drop, + SourceLabels: model.LabelNames{"job"}, + Regex: relabel.MustNewRegexp("(.*)some-[regex]"), + Separator: ";", + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.Drop, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"__address__"}, - TargetLabel: "__tmp_hash", - Regex: relabel.DefaultRelabelConfig.Regex, - Replacement: relabel.DefaultRelabelConfig.Replacement, - Modulus: 8, - Separator: ";", - Action: relabel.HashMod, + SourceLabels: model.LabelNames{"__address__"}, + TargetLabel: "__tmp_hash", + Regex: relabel.DefaultRelabelConfig.Regex, + Replacement: relabel.DefaultRelabelConfig.Replacement, + Modulus: 8, + Separator: ";", + Action: relabel.HashMod, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"__tmp_hash"}, - Regex: relabel.MustNewRegexp("1"), - Separator: ";", - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.Keep, + SourceLabels: model.LabelNames{"__tmp_hash"}, + Regex: relabel.MustNewRegexp("1"), + Separator: ";", + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.Keep, + NameValidationScheme: model.UTF8Validation, }, { - Regex: relabel.MustNewRegexp("1"), - Separator: ";", - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.LabelMap, + Regex: relabel.MustNewRegexp("1"), + Separator: ";", + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.LabelMap, + NameValidationScheme: model.UTF8Validation, }, { - Regex: relabel.MustNewRegexp("d"), - Separator: ";", - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.LabelDrop, + Regex: relabel.MustNewRegexp("d"), + Separator: ";", + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.LabelDrop, + NameValidationScheme: model.UTF8Validation, }, { - Regex: relabel.MustNewRegexp("k"), - Separator: ";", - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.LabelKeep, + Regex: relabel.MustNewRegexp("k"), + Separator: ";", + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.LabelKeep, + NameValidationScheme: model.UTF8Validation, }, }, MetricRelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"__name__"}, - Regex: relabel.MustNewRegexp("expensive_metric.*"), - Separator: ";", - Replacement: relabel.DefaultRelabelConfig.Replacement, - Action: relabel.Drop, + SourceLabels: model.LabelNames{"__name__"}, + Regex: relabel.MustNewRegexp("expensive_metric.*"), + Separator: ";", + Replacement: relabel.DefaultRelabelConfig.Replacement, + Action: relabel.Drop, + NameValidationScheme: model.UTF8Validation, }, }, }, @@ -479,12 +496,13 @@ var expectedConf = &Config{ RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"__meta_sd_consul_tags"}, - Regex: relabel.MustNewRegexp("label:([^=]+)=([^,]+)"), - Separator: ",", - TargetLabel: "${1}", - Replacement: "${2}", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"__meta_sd_consul_tags"}, + Regex: relabel.MustNewRegexp("label:([^=]+)=([^,]+)"), + Separator: ",", + TargetLabel: "${1}", + Replacement: "${2}", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, }, }, @@ -1276,12 +1294,13 @@ var expectedConf = &Config{ RelabelConfigs: []*relabel.Config{ { - Action: relabel.Uppercase, - Regex: relabel.DefaultRelabelConfig.Regex, - Replacement: relabel.DefaultRelabelConfig.Replacement, - Separator: relabel.DefaultRelabelConfig.Separator, - SourceLabels: model.LabelNames{"instance"}, - TargetLabel: "instance", + Action: relabel.Uppercase, + Regex: relabel.DefaultRelabelConfig.Regex, + Replacement: relabel.DefaultRelabelConfig.Replacement, + Separator: relabel.DefaultRelabelConfig.Separator, + SourceLabels: model.LabelNames{"instance"}, + TargetLabel: "instance", + NameValidationScheme: model.UTF8Validation, }, }, @@ -1953,7 +1972,14 @@ func TestLoadConfig(t *testing.T) { c, err := LoadFile("testdata/conf.good.yml", false, promslog.NewNopLogger()) require.NoError(t, err) - require.Equal(t, expectedConf, c) + testutil.RequireEqualWithOptions(t, expectedConf, c, []cmp.Option{ + cmpopts.IgnoreUnexported(config.ProxyConfig{}), + cmpopts.IgnoreUnexported(ionos.SDConfig{}), + cmpopts.IgnoreUnexported(stackit.SDConfig{}), + cmpopts.IgnoreUnexported(regexp.Regexp{}), + cmpopts.IgnoreUnexported(hetzner.SDConfig{}), + cmpopts.IgnoreUnexported(Config{}), + }) } func TestScrapeIntervalLarger(t *testing.T) { diff --git a/go.mod b/go.mod index e993819936..4e57239b9e 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/prometheus/alertmanager v0.28.1 github.com/prometheus/client_golang v1.23.0-rc.1 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 + github.com/prometheus/common v0.65.1-0.20250801071412-c79a891c6c28 github.com/prometheus/common/assets v0.2.0 github.com/prometheus/exporter-toolkit v0.14.0 github.com/prometheus/sigv4 v0.2.0 diff --git a/go.sum b/go.sum index 14a9b22ff1..99e02c2584 100644 --- a/go.sum +++ b/go.sum @@ -454,8 +454,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 h1:R/zO7ombSHCI8bjQusgCMSL+cE669w5/R2upq5WlPD0= -github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.65.1-0.20250801071412-c79a891c6c28 h1:9CaJtf5ZS3GQVCVoslEkJcKSVwiD9aTqwgMpG1n9zQw= +github.com/prometheus/common v0.65.1-0.20250801071412-c79a891c6c28/go.mod h1:LL3lcZII3UXGO4InbF+BTSsiAAPUBnwFVbp4gBWIMqw= github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= diff --git a/model/labels/labels_common.go b/model/labels/labels_common.go index 5f46d6c35f..8169d038f5 100644 --- a/model/labels/labels_common.go +++ b/model/labels/labels_common.go @@ -53,7 +53,7 @@ func (ls Labels) String() string { b.WriteByte(',') b.WriteByte(' ') } - if !model.LabelName(l.Name).IsValidLegacy() { + if !model.LegacyValidation.IsValidLabelName(l.Name) { b.Write(strconv.AppendQuote(b.AvailableBuffer(), l.Name)) } else { b.WriteString(l.Name) @@ -106,18 +106,11 @@ func (ls Labels) IsValid(validationScheme model.ValidationScheme) bool { if l.Name == model.MetricNameLabel { // If the default validation scheme has been overridden with legacy mode, // we need to call the special legacy validation checker. - if validationScheme == model.LegacyValidation && !model.IsValidLegacyMetricName(string(model.LabelValue(l.Value))) { - return strconv.ErrSyntax - } - if !model.IsValidMetricName(model.LabelValue(l.Value)) { + if !validationScheme.IsValidMetricName(l.Value) { return strconv.ErrSyntax } } - if validationScheme == model.LegacyValidation { - if !model.LabelName(l.Name).IsValidLegacy() || !model.LabelValue(l.Value).IsValid() { - return strconv.ErrSyntax - } - } else if !model.LabelName(l.Name).IsValid() || !model.LabelValue(l.Value).IsValid() { + if !validationScheme.IsValidLabelName(l.Name) || !model.LabelValue(l.Value).IsValid() { return strconv.ErrSyntax } return nil diff --git a/model/relabel/relabel.go b/model/relabel/relabel.go index 70daef426f..d6e809bc6f 100644 --- a/model/relabel/relabel.go +++ b/model/relabel/relabel.go @@ -100,6 +100,8 @@ type Config struct { Replacement string `yaml:"replacement,omitempty" json:"replacement,omitempty"` // Action is the action to be performed for the relabeling. Action Action `yaml:"action,omitempty" json:"action,omitempty"` + // NameValidationScheme to use when validating labels. + NameValidationScheme model.ValidationScheme `yaml:"-" json:"-"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -112,10 +114,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if c.Regex.Regexp == nil { c.Regex = MustNewRegexp("") } - return c.Validate() + return nil } -func (c *Config) Validate() error { +func (c *Config) Validate(nameValidationScheme model.ValidationScheme) error { if c.Action == "" { return errors.New("relabel action cannot be empty") } @@ -125,7 +127,17 @@ 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 && !varInRegexTemplate(c.TargetLabel) && !model.LabelName(c.TargetLabel).IsValid() { + + // Relabel config validation scheme matches global if left blank. + switch c.NameValidationScheme { + case model.LegacyValidation, model.UTF8Validation: + case model.UnsetValidation: + c.NameValidationScheme = nameValidationScheme + default: + return fmt.Errorf("unknown relabel config name validation method specified, must be either '', 'legacy' or 'utf8', got %s", c.NameValidationScheme) + } + + if c.Action == Replace && !varInRegexTemplate(c.TargetLabel) && !c.NameValidationScheme.IsValidLabelName(c.TargetLabel) { return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) } @@ -133,12 +145,12 @@ func (c *Config) Validate() error { // UTF-8 allows ${} characters, so standard validation allow $variables by default. // TODO(bwplotka): Relabelling users cannot put $ and ${<...>} characters in metric names or values. // Design escaping mechanism to allow that, once valid use case appears. - return model.LabelName(value).IsValid() + return c.NameValidationScheme.IsValidLabelName(value) } if c.Action == Replace && varInRegexTemplate(c.TargetLabel) && !isValidLabelNameWithRegexVarFn(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() { + if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && !c.NameValidationScheme.IsValidLabelName(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) && c.Replacement != DefaultRelabelConfig.Replacement { @@ -147,7 +159,7 @@ func (c *Config) Validate() error { if c.Action == LabelMap && !isValidLabelNameWithRegexVarFn(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() { + if c.Action == HashMod && !c.NameValidationScheme.IsValidLabelName(c.TargetLabel) { return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) } @@ -318,16 +330,16 @@ func relabel(cfg *Config, lb *labels.Builder) (keep bool) { if indexes == nil { break } - target := model.LabelName(cfg.Regex.ExpandString([]byte{}, cfg.TargetLabel, val, indexes)) - if !target.IsValid() { + target := string(cfg.Regex.ExpandString([]byte{}, cfg.TargetLabel, val, indexes)) + if !cfg.NameValidationScheme.IsValidLabelName(target) { break } res := cfg.Regex.ExpandString([]byte{}, cfg.Replacement, val, indexes) if len(res) == 0 { - lb.Del(string(target)) + lb.Del(target) break } - lb.Set(string(target), string(res)) + lb.Set(target, string(res)) case Lowercase: lb.Set(cfg.TargetLabel, strings.ToLower(val)) case Uppercase: diff --git a/model/relabel/relabel_test.go b/model/relabel/relabel_test.go index 845e304f3b..c3af43e53a 100644 --- a/model/relabel/relabel_test.go +++ b/model/relabel/relabel_test.go @@ -747,7 +747,8 @@ func TestRelabel(t *testing.T) { if cfg.Replacement == "" { cfg.Replacement = DefaultRelabelConfig.Replacement } - require.NoError(t, cfg.Validate()) + cfg.NameValidationScheme = model.UTF8Validation + require.NoError(t, cfg.Validate(model.UTF8Validation)) } res, keep := Process(test.input, test.relabel...) @@ -764,59 +765,76 @@ func TestRelabelValidate(t *testing.T) { expected string }{ { - config: Config{}, + config: Config{ + NameValidationScheme: model.UTF8Validation, + }, expected: `relabel action cannot be empty`, }, { config: Config{ - Action: Replace, + Action: Replace, + NameValidationScheme: model.UTF8Validation, }, expected: `requires 'target_label' value`, }, { config: Config{ - Action: Lowercase, + Action: Lowercase, + NameValidationScheme: model.UTF8Validation, }, expected: `requires 'target_label' value`, }, { config: Config{ - Action: Lowercase, - Replacement: DefaultRelabelConfig.Replacement, - TargetLabel: "${3}", // With UTF-8 naming, this is now a legal relabel rule. + Action: Lowercase, + Replacement: DefaultRelabelConfig.Replacement, + TargetLabel: "${3}", // With UTF-8 naming, this is now a legal relabel rule. + NameValidationScheme: model.UTF8Validation, }, }, { config: Config{ - SourceLabels: model.LabelNames{"a"}, - Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), - Action: Replace, - Replacement: "${1}", - TargetLabel: "${3}", + Action: Lowercase, + Replacement: DefaultRelabelConfig.Replacement, + TargetLabel: "${3}", // Fails with legacy validation + NameValidationScheme: model.LegacyValidation, + }, + expected: "\"${3}\" is invalid 'target_label' for lowercase action", + }, + { + config: Config{ + SourceLabels: model.LabelNames{"a"}, + Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), + Action: Replace, + Replacement: "${1}", + TargetLabel: "${3}", + NameValidationScheme: model.UTF8Validation, }, }, { config: Config{ - SourceLabels: model.LabelNames{"a"}, - Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), - Action: Replace, - Replacement: "${1}", - TargetLabel: "0${3}", // With UTF-8 naming this targets a valid label. + SourceLabels: model.LabelNames{"a"}, + Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), + Action: Replace, + Replacement: "${1}", + TargetLabel: "0${3}", // With UTF-8 naming this targets a valid label. + NameValidationScheme: model.UTF8Validation, }, }, { config: Config{ - SourceLabels: model.LabelNames{"a"}, - Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), - Action: Replace, - Replacement: "${1}", - TargetLabel: "-${3}", // With UTF-8 naming this targets a valid label. + SourceLabels: model.LabelNames{"a"}, + Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), + Action: Replace, + Replacement: "${1}", + TargetLabel: "-${3}", // With UTF-8 naming this targets a valid label. + NameValidationScheme: model.UTF8Validation, }, }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { - err := test.config.Validate() + err := test.config.Validate(model.UTF8Validation) if test.expected == "" { require.NoError(t, err) } else { diff --git a/model/rulefmt/rulefmt.go b/model/rulefmt/rulefmt.go index 9b1c897a98..1dc6808612 100644 --- a/model/rulefmt/rulefmt.go +++ b/model/rulefmt/rulefmt.go @@ -96,7 +96,14 @@ type ruleGroups struct { } // Validate validates all rules in the rule groups. -func (g *RuleGroups) Validate(node ruleGroups) (errs []error) { +func (g *RuleGroups) Validate(node ruleGroups, nameValidationScheme model.ValidationScheme) (errs []error) { + switch nameValidationScheme { + case model.UTF8Validation, model.LegacyValidation: + default: + errs = append(errs, fmt.Errorf("unhandled nameValidationScheme: %s", nameValidationScheme)) + return + } + set := map[string]struct{}{} for j, g := range g.Groups { @@ -112,7 +119,7 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) { } for k, v := range g.Labels { - if !model.LabelName(k).IsValid() || k == model.MetricNameLabel { + if !nameValidationScheme.IsValidLabelName(k) || k == model.MetricNameLabel { errs = append( errs, fmt.Errorf("invalid label name: %s", k), ) @@ -128,7 +135,7 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) { set[g.Name] = struct{}{} for i, r := range g.Rules { - for _, node := range r.Validate(node.Groups[j].Rules[i]) { + for _, node := range r.Validate(node.Groups[j].Rules[i], nameValidationScheme) { var ruleName string if r.Alert != "" { ruleName = r.Alert @@ -192,7 +199,7 @@ type RuleNode struct { } // Validate the rule and return a list of encountered errors. -func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { +func (r *Rule) Validate(node RuleNode, nameValidationScheme model.ValidationScheme) (nodes []WrappedError) { if r.Record != "" && r.Alert != "" { nodes = append(nodes, WrappedError{ err: errors.New("only one of 'record' and 'alert' must be set"), @@ -238,7 +245,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { node: &node.Record, }) } - if !model.IsValidMetricName(model.LabelValue(r.Record)) { + if !nameValidationScheme.IsValidMetricName(r.Record) { nodes = append(nodes, WrappedError{ err: fmt.Errorf("invalid recording rule name: %s", r.Record), node: &node.Record, @@ -255,7 +262,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { } for k, v := range r.Labels { - if !model.LabelName(k).IsValid() || k == model.MetricNameLabel { + if !nameValidationScheme.IsValidLabelName(k) || k == model.MetricNameLabel { nodes = append(nodes, WrappedError{ err: fmt.Errorf("invalid label name: %s", k), }) @@ -269,7 +276,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { } for k := range r.Annotations { - if !model.LabelName(k).IsValid() { + if !nameValidationScheme.IsValidLabelName(k) { nodes = append(nodes, WrappedError{ err: fmt.Errorf("invalid annotation name: %s", k), }) @@ -333,7 +340,7 @@ func testTemplateParsing(rl *Rule) (errs []error) { } // Parse parses and validates a set of rules. -func Parse(content []byte, ignoreUnknownFields bool) (*RuleGroups, []error) { +func Parse(content []byte, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*RuleGroups, []error) { var ( groups RuleGroups node ruleGroups @@ -358,16 +365,16 @@ func Parse(content []byte, ignoreUnknownFields bool) (*RuleGroups, []error) { return nil, errs } - return &groups, groups.Validate(node) + return &groups, groups.Validate(node, nameValidationScheme) } // ParseFile reads and parses rules from a file. -func ParseFile(file string, ignoreUnknownFields bool) (*RuleGroups, []error) { +func ParseFile(file string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*RuleGroups, []error) { b, err := os.ReadFile(file) if err != nil { return nil, []error{fmt.Errorf("%s: %w", file, err)} } - rgs, errs := Parse(b, ignoreUnknownFields) + rgs, errs := Parse(b, ignoreUnknownFields, nameValidationScheme) for i := range errs { errs[i] = fmt.Errorf("%s: %w", file, errs[i]) } diff --git a/model/rulefmt/rulefmt_test.go b/model/rulefmt/rulefmt_test.go index cc873011cb..45fc0f8227 100644 --- a/model/rulefmt/rulefmt_test.go +++ b/model/rulefmt/rulefmt_test.go @@ -19,17 +19,20 @@ import ( "path/filepath" "testing" + "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) func TestParseFileSuccess(t *testing.T) { - _, errs := ParseFile("testdata/test.yaml", false) + _, errs := ParseFile("testdata/test.yaml", false, model.UTF8Validation) require.Empty(t, errs, "unexpected errors parsing file") - _, errs = ParseFile("testdata/utf-8_lname.good.yaml", false) + _, errs = ParseFile("testdata/utf-8_lname.good.yaml", false, model.UTF8Validation) require.Empty(t, errs, "unexpected errors parsing file") - _, errs = ParseFile("testdata/utf-8_annotation.good.yaml", false) + _, errs = ParseFile("testdata/utf-8_annotation.good.yaml", false, model.UTF8Validation) + require.Empty(t, errs, "unexpected errors parsing file") + _, errs = ParseFile("testdata/legacy_validation_annotation.good.yaml", false, model.LegacyValidation) require.Empty(t, errs, "unexpected errors parsing file") } @@ -38,7 +41,7 @@ func TestParseFileSuccessWithAliases(t *testing.T) { / sum without(instance) (rate(requests_total[5m])) ` - rgs, errs := ParseFile("testdata/test_aliases.yaml", false) + rgs, errs := ParseFile("testdata/test_aliases.yaml", false, model.UTF8Validation) require.Empty(t, errs, "unexpected errors parsing file") for _, rg := range rgs.Groups { require.Equal(t, "HighAlert", rg.Rules[0].Alert) @@ -62,8 +65,9 @@ sum without(instance) (rate(requests_total[5m])) func TestParseFileFailure(t *testing.T) { for _, c := range []struct { - filename string - errMsg string + filename string + errMsg string + nameValidationScheme model.ValidationScheme }{ { filename: "duplicate_grp.bad.yaml", @@ -105,9 +109,17 @@ func TestParseFileFailure(t *testing.T) { filename: "record_and_keep_firing_for.bad.yaml", errMsg: "invalid field 'keep_firing_for' in recording rule", }, + { + filename: "legacy_validation_annotation.bad.yaml", + nameValidationScheme: model.LegacyValidation, + errMsg: "invalid annotation name: ins-tance", + }, } { t.Run(c.filename, func(t *testing.T) { - _, errs := ParseFile(filepath.Join("testdata", c.filename), false) + if c.nameValidationScheme == model.UnsetValidation { + c.nameValidationScheme = model.UTF8Validation + } + _, errs := ParseFile(filepath.Join("testdata", c.filename), false, c.nameValidationScheme) require.NotEmpty(t, errs, "Expected error parsing %s but got none", c.filename) require.ErrorContainsf(t, errs[0], c.errMsg, "Expected error for %s.", c.filename) }) @@ -203,7 +215,7 @@ groups: } for _, tst := range tests { - rgs, errs := Parse([]byte(tst.ruleString), false) + rgs, errs := Parse([]byte(tst.ruleString), false, model.UTF8Validation) require.NotNil(t, rgs, "Rule parsing, rule=\n"+tst.ruleString) passed := (tst.shouldPass && len(errs) == 0) || (!tst.shouldPass && len(errs) > 0) require.True(t, passed, "Rule validation failed, rule=\n"+tst.ruleString) @@ -230,7 +242,7 @@ groups: annotations: summary: "Instance {{ $labels.instance }} up" ` - _, errs := Parse([]byte(group), false) + _, errs := Parse([]byte(group), false, model.UTF8Validation) require.Len(t, errs, 2, "Expected two errors") var err00 *Error require.ErrorAs(t, errs[0], &err00) diff --git a/model/rulefmt/testdata/legacy_validation_annotation.bad.yaml b/model/rulefmt/testdata/legacy_validation_annotation.bad.yaml new file mode 100644 index 0000000000..a747827a96 --- /dev/null +++ b/model/rulefmt/testdata/legacy_validation_annotation.bad.yaml @@ -0,0 +1,7 @@ +groups: + - name: yolo + rules: + - alert: hola + expr: 1 + annotations: + ins-tance: localhost diff --git a/model/rulefmt/testdata/legacy_validation_annotation.good.yaml b/model/rulefmt/testdata/legacy_validation_annotation.good.yaml new file mode 100644 index 0000000000..758be9d67b --- /dev/null +++ b/model/rulefmt/testdata/legacy_validation_annotation.good.yaml @@ -0,0 +1,7 @@ +groups: + - name: yolo + rules: + - alert: hola + expr: 1 + annotations: + ins_tance: localhost diff --git a/model/textparse/protobufparse.go b/model/textparse/protobufparse.go index 2ca6c03af7..8dd13f5b7a 100644 --- a/model/textparse/protobufparse.go +++ b/model/textparse/protobufparse.go @@ -428,7 +428,7 @@ func (p *ProtobufParser) Next() (Entry, error) { // We are at the beginning of a metric family. Put only the name // into entryBytes and validate only name, help, and type for now. name := p.dec.GetName() - if !model.IsValidMetricName(model.LabelValue(name)) { + if !model.UTF8Validation.IsValidMetricName(name) { return EntryInvalid, fmt.Errorf("invalid metric name: %s", name) } if help := p.dec.GetHelp(); !utf8.ValidString(help) { diff --git a/notifier/manager.go b/notifier/manager.go index c9463b24a8..65adfd5c3e 100644 --- a/notifier/manager.go +++ b/notifier/manager.go @@ -26,6 +26,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/model" "github.com/prometheus/common/promslog" "github.com/prometheus/common/version" @@ -92,7 +93,7 @@ func do(ctx context.Context, client *http.Client, req *http.Request) (*http.Resp } // NewManager is the manager constructor. -func NewManager(o *Options, logger *slog.Logger) *Manager { +func NewManager(o *Options, nameValidationScheme model.ValidationScheme, logger *slog.Logger) *Manager { if o.Do == nil { o.Do = do } @@ -104,6 +105,14 @@ func NewManager(o *Options, logger *slog.Logger) *Manager { logger = promslog.NewNopLogger() } + for _, rc := range o.RelabelConfigs { + switch rc.NameValidationScheme { + case model.LegacyValidation, model.UTF8Validation: + default: + rc.NameValidationScheme = nameValidationScheme + } + } + n := &Manager{ queue: make([]*Alert, 0, o.QueueCapacity), more: make(chan struct{}, 1), @@ -133,6 +142,13 @@ func (n *Manager) ApplyConfig(conf *config.Config) error { n.opts.ExternalLabels = conf.GlobalConfig.ExternalLabels n.opts.RelabelConfigs = conf.AlertingConfig.AlertRelabelConfigs + for i, rc := range n.opts.RelabelConfigs { + switch rc.NameValidationScheme { + case model.LegacyValidation, model.UTF8Validation: + default: + n.opts.RelabelConfigs[i].NameValidationScheme = conf.GlobalConfig.MetricNameValidationScheme + } + } amSets := make(map[string]*alertmanagerSet) // configToAlertmanagers maps alertmanager sets for each unique AlertmanagerConfig, diff --git a/notifier/manager_test.go b/notifier/manager_test.go index fd553ad191..6dfed577d6 100644 --- a/notifier/manager_test.go +++ b/notifier/manager_test.go @@ -45,7 +45,7 @@ import ( const maxBatchSize = 256 func TestHandlerNextBatch(t *testing.T) { - h := NewManager(&Options{}, nil) + h := NewManager(&Options{}, model.UTF8Validation, nil) for i := range make([]struct{}, 2*maxBatchSize+1) { h.queue = append(h.queue, &Alert{ @@ -125,7 +125,7 @@ func TestHandlerSendAll(t *testing.T) { defer server2.Close() defer server3.Close() - h := NewManager(&Options{}, nil) + h := NewManager(&Options{}, model.UTF8Validation, nil) authClient, _ := config_util.NewClientFromConfig( config_util.HTTPClientConfig{ @@ -235,7 +235,7 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) { defer server2.Close() defer server3.Close() - h := NewManager(&Options{}, nil) + h := NewManager(&Options{}, model.UTF8Validation, nil) h.alertmanagers = make(map[string]*alertmanagerSet) am1Cfg := config.DefaultAlertmanagerConfig @@ -245,9 +245,10 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) { am2Cfg.Timeout = model.Duration(time.Second) am2Cfg.AlertRelabelConfigs = []*relabel.Config{ { - SourceLabels: model.LabelNames{"alertnamedrop"}, - Action: "drop", - Regex: relabel.MustNewRegexp(".+"), + SourceLabels: model.LabelNames{"alertnamedrop"}, + Action: "drop", + Regex: relabel.MustNewRegexp(".+"), + NameValidationScheme: model.UTF8Validation, }, } @@ -255,9 +256,10 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) { am3Cfg.Timeout = model.Duration(time.Second) am3Cfg.AlertRelabelConfigs = []*relabel.Config{ { - SourceLabels: model.LabelNames{"alertname"}, - Action: "drop", - Regex: relabel.MustNewRegexp(".+"), + SourceLabels: model.LabelNames{"alertname"}, + Action: "drop", + Regex: relabel.MustNewRegexp(".+"), + NameValidationScheme: model.UTF8Validation, }, } @@ -374,7 +376,7 @@ func TestCustomDo(t *testing.T) { Body: io.NopCloser(bytes.NewBuffer(nil)), }, nil }, - }, nil) + }, model.UTF8Validation, nil) h.sendOne(context.Background(), nil, testURL, []byte(testBody)) @@ -388,14 +390,15 @@ func TestExternalLabels(t *testing.T) { ExternalLabels: labels.FromStrings("a", "b"), RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"alertname"}, - TargetLabel: "a", - Action: "replace", - Regex: relabel.MustNewRegexp("externalrelabelthis"), - Replacement: "c", + SourceLabels: model.LabelNames{"alertname"}, + TargetLabel: "a", + Action: "replace", + Regex: relabel.MustNewRegexp("externalrelabelthis"), + Replacement: "c", + NameValidationScheme: model.UTF8Validation, }, }, - }, nil) + }, model.UTF8Validation, nil) // This alert should get the external label attached. h.Send(&Alert{ @@ -422,19 +425,21 @@ func TestHandlerRelabel(t *testing.T) { MaxBatchSize: maxBatchSize, RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"alertname"}, - Action: "drop", - Regex: relabel.MustNewRegexp("drop"), + SourceLabels: model.LabelNames{"alertname"}, + Action: "drop", + Regex: relabel.MustNewRegexp("drop"), + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"alertname"}, - TargetLabel: "alertname", - Action: "replace", - Regex: relabel.MustNewRegexp("rename"), - Replacement: "renamed", + SourceLabels: model.LabelNames{"alertname"}, + TargetLabel: "alertname", + Action: "replace", + Regex: relabel.MustNewRegexp("rename"), + Replacement: "renamed", + NameValidationScheme: model.UTF8Validation, }, }, - }, nil) + }, model.UTF8Validation, nil) // This alert should be dropped due to the configuration h.Send(&Alert{ @@ -500,6 +505,7 @@ func TestHandlerQueuing(t *testing.T) { QueueCapacity: 3 * maxBatchSize, MaxBatchSize: maxBatchSize, }, + model.UTF8Validation, nil, ) @@ -606,7 +612,7 @@ func TestReload(t *testing.T) { }, } - n := NewManager(&Options{}, nil) + n := NewManager(&Options{}, model.UTF8Validation, nil) cfg := &config.Config{} s := ` @@ -653,7 +659,7 @@ func TestDroppedAlertmanagers(t *testing.T) { }, } - n := NewManager(&Options{}, nil) + n := NewManager(&Options{}, model.UTF8Validation, nil) cfg := &config.Config{} s := ` @@ -766,6 +772,7 @@ func TestHangingNotifier(t *testing.T) { &Options{ QueueCapacity: alertsCount, }, + model.UTF8Validation, nil, ) notifier.alertmanagers = make(map[string]*alertmanagerSet) @@ -883,6 +890,7 @@ func TestStop_DrainingDisabled(t *testing.T) { QueueCapacity: 10, DrainOnShutdown: false, }, + model.UTF8Validation, nil, ) @@ -969,6 +977,7 @@ func TestStop_DrainingEnabled(t *testing.T) { QueueCapacity: 10, DrainOnShutdown: true, }, + model.UTF8Validation, nil, ) @@ -1031,7 +1040,7 @@ func TestApplyConfig(t *testing.T) { } alertmanagerURL := fmt.Sprintf("http://%s/api/v2/alerts", targetURL) - n := NewManager(&Options{}, nil) + n := NewManager(&Options{}, model.UTF8Validation, nil) cfg := &config.Config{} s := ` alerting: diff --git a/prompb/io/prometheus/client/decoder.go b/prompb/io/prometheus/client/decoder.go index 983803846e..1288a41042 100644 --- a/prompb/io/prometheus/client/decoder.go +++ b/prompb/io/prometheus/client/decoder.go @@ -177,6 +177,7 @@ func (m *MetricStreamingDecoder) Label(b scratchBuilder) error { // via UnsafeAddBytes method to reuse strings. func parseLabel(dAtA []byte, b scratchBuilder) error { var name, value []byte + var unsafeName string l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -236,8 +237,9 @@ func parseLabel(dAtA []byte, b scratchBuilder) error { return io.ErrUnexpectedEOF } name = dAtA[iNdEx:postIndex] - if !model.LabelName(name).IsValid() { - return fmt.Errorf("invalid label name: %s", name) + unsafeName = yoloString(name) + if !model.UTF8Validation.IsValidLabelName(unsafeName) { + return fmt.Errorf("invalid label name: %s", unsafeName) } iNdEx = postIndex case 2: diff --git a/promql/engine.go b/promql/engine.go index 3cdf299dff..c493bd5a45 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1676,7 +1676,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value, if e.Op == parser.COUNT_VALUES { valueLabel := param.(*parser.StringLiteral) - if !model.LabelName(valueLabel.Val).IsValid() { + if !model.UTF8Validation.IsValidLabelName(valueLabel.Val) { ev.errorf("invalid label name %s", valueLabel) } if !e.Without { diff --git a/promql/functions.go b/promql/functions.go index 6b038fe336..6eb06e1d04 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1584,7 +1584,7 @@ func (ev *evaluator) evalLabelReplace(ctx context.Context, args parser.Expressio if err != nil { panic(fmt.Errorf("invalid regular expression in label_replace(): %s", regexStr)) } - if !model.LabelName(dst).IsValid() { + if !model.UTF8Validation.IsValidLabelName(dst) { panic(fmt.Errorf("invalid destination label name in label_replace(): %s", dst)) } @@ -1632,12 +1632,12 @@ func (ev *evaluator) evalLabelJoin(ctx context.Context, args parser.Expressions) ) for i := 3; i < len(args); i++ { src := stringFromArg(args[i]) - if !model.LabelName(src).IsValid() { + if !model.UTF8Validation.IsValidLabelName(src) { panic(fmt.Errorf("invalid source label name in label_join(): %s", src)) } srcLabels[i-3] = src } - if !model.LabelName(dst).IsValid() { + if !model.UTF8Validation.IsValidLabelName(dst) { panic(fmt.Errorf("invalid destination label name in label_join(): %s", dst)) } diff --git a/promql/parser/generated_parser.y b/promql/parser/generated_parser.y index e7e16cd033..285317fcc3 100644 --- a/promql/parser/generated_parser.y +++ b/promql/parser/generated_parser.y @@ -378,14 +378,14 @@ grouping_label_list: grouping_label : maybe_label { - if !model.LabelName($1.Val).IsValid() { + if !model.UTF8Validation.IsValidLabelName($1.Val) { yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", $1.Val) } $$ = $1 } | STRING { unquoted := yylex.(*parser).unquoteString($1.Val) - if !model.LabelName(unquoted).IsValid() { + if !model.UTF8Validation.IsValidLabelName(unquoted) { yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", unquoted) } $$ = $1 diff --git a/promql/parser/generated_parser.y.go b/promql/parser/generated_parser.y.go index e93d1b3de6..942d33d4b7 100644 --- a/promql/parser/generated_parser.y.go +++ b/promql/parser/generated_parser.y.go @@ -1327,7 +1327,7 @@ yydefault: case 59: yyDollar = yyS[yypt-1 : yypt+1] { - if !model.LabelName(yyDollar[1].item.Val).IsValid() { + if !model.UTF8Validation.IsValidLabelName(yyDollar[1].item.Val) { yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", yyDollar[1].item.Val) } yyVAL.item = yyDollar[1].item @@ -1336,7 +1336,7 @@ yydefault: yyDollar = yyS[yypt-1 : yypt+1] { unquoted := yylex.(*parser).unquoteString(yyDollar[1].item.Val) - if !model.LabelName(unquoted).IsValid() { + if !model.UTF8Validation.IsValidLabelName(unquoted) { yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", unquoted) } yyVAL.item = yyDollar[1].item diff --git a/promql/parser/printer.go b/promql/parser/printer.go index 9dae10a70e..a8c7dcea2d 100644 --- a/promql/parser/printer.go +++ b/promql/parser/printer.go @@ -100,7 +100,7 @@ func joinLabels(ss []string) string { if i > 0 { b.WriteString(", ") } - if !model.IsValidLegacyMetricName(string(model.LabelValue(s))) { + if !model.LegacyValidation.IsValidMetricName(s) { b.Write(strconv.AppendQuote(b.AvailableBuffer(), s)) } else { b.WriteString(s) diff --git a/rules/alerting_test.go b/rules/alerting_test.go index 9d8e10711b..de92bf57a0 100644 --- a/rules/alerting_test.go +++ b/rules/alerting_test.go @@ -771,15 +771,16 @@ func TestSendAlertsDontAffectActiveAlerts(t *testing.T) { QueueCapacity: 1, RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"a1"}, - Regex: relabel.MustNewRegexp("(.+)"), - TargetLabel: "a1", - Replacement: "bug", - Action: "replace", + SourceLabels: model.LabelNames{"a1"}, + Regex: relabel.MustNewRegexp("(.+)"), + TargetLabel: "a1", + Replacement: "bug", + Action: "replace", + NameValidationScheme: model.UTF8Validation, }, }, } - nm := notifier.NewManager(&opts, promslog.NewNopLogger()) + nm := notifier.NewManager(&opts, model.UTF8Validation, promslog.NewNopLogger()) f := SendAlerts(nm, "") notifyFunc := func(ctx context.Context, expr string, alerts ...*Alert) { diff --git a/rules/group.go b/rules/group.go index ed727ff983..9b2912dc2d 100644 --- a/rules/group.go +++ b/rules/group.go @@ -98,7 +98,9 @@ type GroupOptions struct { func NewGroup(o GroupOptions) *Group { opts := o.Opts if opts == nil { - opts = &ManagerOptions{} + opts = &ManagerOptions{ + NameValidationScheme: model.UTF8Validation, + } } metrics := opts.Metrics if metrics == nil { diff --git a/rules/manager.go b/rules/manager.go index 7cbe3ce15a..dbb5d38498 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -26,6 +26,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/model" "github.com/prometheus/common/promslog" "golang.org/x/sync/semaphore" @@ -107,6 +108,7 @@ type NotifyFunc func(ctx context.Context, expr string, alerts ...*Alert) // ManagerOptions bundles options for the Manager. type ManagerOptions struct { + NameValidationScheme model.ValidationScheme ExternalURL *url.URL QueryFunc QueryFunc NotifyFunc NotifyFunc @@ -289,7 +291,7 @@ func (m *Manager) Update(interval time.Duration, files []string, externalLabels // GroupLoader is responsible for loading rule groups from arbitrary sources and parsing them. type GroupLoader interface { - Load(identifier string, ignoreUnknownFields bool) (*rulefmt.RuleGroups, []error) + Load(identifier string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*rulefmt.RuleGroups, []error) Parse(query string) (parser.Expr, error) } @@ -297,8 +299,8 @@ type GroupLoader interface { // and parser.ParseExpr. type FileLoader struct{} -func (FileLoader) Load(identifier string, ignoreUnknownFields bool) (*rulefmt.RuleGroups, []error) { - return rulefmt.ParseFile(identifier, ignoreUnknownFields) +func (FileLoader) Load(identifier string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*rulefmt.RuleGroups, []error) { + return rulefmt.ParseFile(identifier, ignoreUnknownFields, nameValidationScheme) } func (FileLoader) Parse(query string) (parser.Expr, error) { return parser.ParseExpr(query) } @@ -312,7 +314,7 @@ func (m *Manager) LoadGroups( shouldRestore := !m.restored || m.restoreNewRuleGroups for _, fn := range filenames { - rgs, errs := m.opts.GroupLoader.Load(fn, ignoreUnknownFields) + rgs, errs := m.opts.GroupLoader.Load(fn, ignoreUnknownFields, m.opts.NameValidationScheme) if errs != nil { return nil, errs } @@ -582,7 +584,7 @@ func FromMaps(maps ...map[string]string) labels.Labels { } // ParseFiles parses the rule files corresponding to glob patterns. -func ParseFiles(patterns []string) error { +func ParseFiles(patterns []string, nameValidationScheme model.ValidationScheme) error { files := map[string]string{} for _, pat := range patterns { fns, err := filepath.Glob(pat) @@ -602,7 +604,7 @@ func ParseFiles(patterns []string) error { } } for fn, pat := range files { - _, errs := rulefmt.ParseFile(fn, false) + _, errs := rulefmt.ParseFile(fn, false, nameValidationScheme) if len(errs) > 0 { return fmt.Errorf("parse rules from file %q (pattern: %q): %w", fn, pat, errors.Join(errs...)) } diff --git a/rules/manager_test.go b/rules/manager_test.go index aa8fed59f8..8504f1f114 100644 --- a/rules/manager_test.go +++ b/rules/manager_test.go @@ -372,14 +372,15 @@ func TestForStateRestore(t *testing.T) { ng := testEngine(t) opts := &ManagerOptions{ - QueryFunc: EngineQueryFunc(ng, storage), - Appendable: storage, - Queryable: storage, - Context: context.Background(), - Logger: promslog.NewNopLogger(), - NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {}, - OutageTolerance: 30 * time.Minute, - ForGracePeriod: 10 * time.Minute, + QueryFunc: EngineQueryFunc(ng, storage), + Appendable: storage, + Queryable: storage, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {}, + OutageTolerance: 30 * time.Minute, + ForGracePeriod: 10 * time.Minute, + NameValidationScheme: model.UTF8Validation, } alertForDuration := 25 * time.Minute @@ -545,11 +546,12 @@ func TestStaleness(t *testing.T) { } engine := promqltest.NewTestEngineWithOpts(t, engineOpts) opts := &ManagerOptions{ - QueryFunc: EngineQueryFunc(engine, st), - Appendable: st, - Queryable: st, - Context: context.Background(), - Logger: promslog.NewNopLogger(), + QueryFunc: EngineQueryFunc(engine, st), + Appendable: st, + Queryable: st, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("a + 1") @@ -647,6 +649,7 @@ groups: DefaultRuleQueryOffset: func() time.Duration { return time.Minute }, + NameValidationScheme: model.UTF8Validation, }) m.start() err = m.Update(time.Second, []string{fname}, labels.EmptyLabels(), "", nil) @@ -739,6 +742,7 @@ func TestDeletedRuleMarkedStale(t *testing.T) { opts: &ManagerOptions{ Appendable: st, RuleConcurrencyController: sequentialRuleEvalController{}, + NameValidationScheme: model.UTF8Validation, }, metrics: NewGroupMetrics(nil), } @@ -779,11 +783,12 @@ func TestUpdate(t *testing.T) { } engine := promqltest.NewTestEngineWithOpts(t, opts) ruleManager := NewManager(&ManagerOptions{ - Appendable: st, - Queryable: st, - QueryFunc: EngineQueryFunc(engine, st), - Context: context.Background(), - Logger: promslog.NewNopLogger(), + Appendable: st, + Queryable: st, + QueryFunc: EngineQueryFunc(engine, st), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, }) ruleManager.start() defer ruleManager.Stop() @@ -810,7 +815,7 @@ func TestUpdate(t *testing.T) { } // Groups will be recreated if updated. - rgs, errs := rulefmt.ParseFile("fixtures/rules.yaml", false) + rgs, errs := rulefmt.ParseFile("fixtures/rules.yaml", false, model.UTF8Validation) require.Empty(t, errs, "file parsing failures") tmpFile, err := os.CreateTemp("", "rules.test.*.yaml") @@ -923,13 +928,14 @@ func TestNotify(t *testing.T) { lastNotified = alerts } opts := &ManagerOptions{ - QueryFunc: EngineQueryFunc(engine, storage), - Appendable: storage, - Queryable: storage, - Context: context.Background(), - Logger: promslog.NewNopLogger(), - NotifyFunc: notifyFunc, - ResendDelay: 2 * time.Second, + QueryFunc: EngineQueryFunc(engine, storage), + Appendable: storage, + Queryable: storage, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NotifyFunc: notifyFunc, + ResendDelay: 2 * time.Second, + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("a > 1") @@ -994,12 +1000,13 @@ func TestMetricsUpdate(t *testing.T) { } engine := promqltest.NewTestEngineWithOpts(t, opts) ruleManager := NewManager(&ManagerOptions{ - Appendable: storage, - Queryable: storage, - QueryFunc: EngineQueryFunc(engine, storage), - Context: context.Background(), - Logger: promslog.NewNopLogger(), - Registerer: registry, + Appendable: storage, + Queryable: storage, + QueryFunc: EngineQueryFunc(engine, storage), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + Registerer: registry, + NameValidationScheme: model.UTF8Validation, }) ruleManager.start() defer ruleManager.Stop() @@ -1065,11 +1072,12 @@ func TestGroupStalenessOnRemoval(t *testing.T) { } engine := promqltest.NewTestEngineWithOpts(t, opts) ruleManager := NewManager(&ManagerOptions{ - Appendable: storage, - Queryable: storage, - QueryFunc: EngineQueryFunc(engine, storage), - Context: context.Background(), - Logger: promslog.NewNopLogger(), + Appendable: storage, + Queryable: storage, + QueryFunc: EngineQueryFunc(engine, storage), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, }) var stopped bool ruleManager.start() @@ -1142,11 +1150,12 @@ func TestMetricsStalenessOnManagerShutdown(t *testing.T) { } engine := promqltest.NewTestEngineWithOpts(t, opts) ruleManager := NewManager(&ManagerOptions{ - Appendable: storage, - Queryable: storage, - QueryFunc: EngineQueryFunc(engine, storage), - Context: context.Background(), - Logger: promslog.NewNopLogger(), + Appendable: storage, + Queryable: storage, + QueryFunc: EngineQueryFunc(engine, storage), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, }) var stopped bool ruleManager.start() @@ -1209,11 +1218,12 @@ func TestRuleMovedBetweenGroups(t *testing.T) { } engine := promql.NewEngine(opts) ruleManager := NewManager(&ManagerOptions{ - Appendable: storage, - Queryable: storage, - QueryFunc: EngineQueryFunc(engine, storage), - Context: context.Background(), - Logger: promslog.NewNopLogger(), + Appendable: storage, + Queryable: storage, + QueryFunc: EngineQueryFunc(engine, storage), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, }) var stopped bool ruleManager.start() @@ -1291,11 +1301,12 @@ func TestRuleHealthUpdates(t *testing.T) { } engine := promqltest.NewTestEngineWithOpts(t, engineOpts) opts := &ManagerOptions{ - QueryFunc: EngineQueryFunc(engine, st), - Appendable: st, - Queryable: st, - Context: context.Background(), - Logger: promslog.NewNopLogger(), + QueryFunc: EngineQueryFunc(engine, st), + Appendable: st, + Queryable: st, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("a + 1") @@ -1389,14 +1400,15 @@ func TestRuleGroupEvalIterationFunc(t *testing.T) { ng := testEngine(t) testFunc := func(tst testInput) { opts := &ManagerOptions{ - QueryFunc: EngineQueryFunc(ng, storage), - Appendable: storage, - Queryable: storage, - Context: context.Background(), - Logger: promslog.NewNopLogger(), - NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {}, - OutageTolerance: 30 * time.Minute, - ForGracePeriod: 10 * time.Minute, + QueryFunc: EngineQueryFunc(ng, storage), + Appendable: storage, + Queryable: storage, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {}, + OutageTolerance: 30 * time.Minute, + ForGracePeriod: 10 * time.Minute, + NameValidationScheme: model.UTF8Validation, } activeAlert := &Alert{ @@ -1473,11 +1485,12 @@ func TestNativeHistogramsInRecordingRules(t *testing.T) { ng := testEngine(t) opts := &ManagerOptions{ - QueryFunc: EngineQueryFunc(ng, storage), - Appendable: storage, - Queryable: storage, - Context: context.Background(), - Logger: promslog.NewNopLogger(), + QueryFunc: EngineQueryFunc(ng, storage), + Appendable: storage, + Queryable: storage, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("sum(histogram_metric)") @@ -1524,10 +1537,11 @@ func TestManager_LoadGroups_ShouldCheckWhetherEachRuleHasDependentsAndDependenci }) ruleManager := NewManager(&ManagerOptions{ - Context: context.Background(), - Logger: promslog.NewNopLogger(), - Appendable: storage, - QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + Appendable: storage, + QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, + NameValidationScheme: model.UTF8Validation, }) t.Run("load a mix of dependent and independent rules", func(t *testing.T) { @@ -1580,8 +1594,9 @@ func TestManager_LoadGroups_ShouldCheckWhetherEachRuleHasDependentsAndDependenci func TestDependencyMap(t *testing.T) { ctx := context.Background() opts := &ManagerOptions{ - Context: ctx, - Logger: promslog.NewNopLogger(), + Context: ctx, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") @@ -1638,8 +1653,9 @@ func TestDependencyMap(t *testing.T) { func TestNoDependency(t *testing.T) { ctx := context.Background() opts := &ManagerOptions{ - Context: ctx, - Logger: promslog.NewNopLogger(), + Context: ctx, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") @@ -1661,8 +1677,9 @@ func TestNoDependency(t *testing.T) { func TestDependenciesEdgeCases(t *testing.T) { ctx := context.Background() opts := &ManagerOptions{ - Context: ctx, - Logger: promslog.NewNopLogger(), + Context: ctx, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } t.Run("empty group", func(t *testing.T) { @@ -1819,8 +1836,9 @@ func TestDependenciesEdgeCases(t *testing.T) { func TestNoMetricSelector(t *testing.T) { ctx := context.Background() opts := &ManagerOptions{ - Context: ctx, - Logger: promslog.NewNopLogger(), + Context: ctx, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") @@ -1848,8 +1866,9 @@ func TestNoMetricSelector(t *testing.T) { func TestDependentRulesWithNonMetricExpression(t *testing.T) { ctx := context.Background() opts := &ManagerOptions{ - Context: ctx, - Logger: promslog.NewNopLogger(), + Context: ctx, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") @@ -1880,8 +1899,9 @@ func TestDependentRulesWithNonMetricExpression(t *testing.T) { func TestRulesDependentOnMetaMetrics(t *testing.T) { ctx := context.Background() opts := &ManagerOptions{ - Context: ctx, - Logger: promslog.NewNopLogger(), + Context: ctx, + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, } // This rule is not dependent on any other rules in its group but it does depend on `ALERTS`, which is produced by @@ -1909,8 +1929,9 @@ func TestRulesDependentOnMetaMetrics(t *testing.T) { func TestDependencyMapUpdatesOnGroupUpdate(t *testing.T) { files := []string{"fixtures/rules.yaml"} ruleManager := NewManager(&ManagerOptions{ - Context: context.Background(), - Logger: promslog.NewNopLogger(), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, }) ruleManager.start() @@ -2447,8 +2468,9 @@ func TestBoundedRuleEvalConcurrency(t *testing.T) { func TestUpdateWhenStopped(t *testing.T) { files := []string{"fixtures/rules.yaml"} ruleManager := NewManager(&ManagerOptions{ - Context: context.Background(), - Logger: promslog.NewNopLogger(), + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NameValidationScheme: model.UTF8Validation, }) ruleManager.start() err := ruleManager.Update(10*time.Second, files, labels.EmptyLabels(), "", nil) @@ -2509,6 +2531,7 @@ func optsFactory(storage storage.Storage, maxInflight, inflightQueries *atomic.I ConcurrentEvalsEnabled: concurrent, MaxConcurrentEvals: maxConcurrent, Appendable: storage, + NameValidationScheme: model.UTF8Validation, QueryFunc: func(_ context.Context, _ string, ts time.Time) (promql.Vector, error) { inflightMu.Lock() @@ -2552,11 +2575,11 @@ func TestLabels_FromMaps(t *testing.T) { func TestParseFiles(t *testing.T) { t.Run("good files", func(t *testing.T) { - err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")}) + err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")}, model.UTF8Validation) require.NoError(t, err) }) t.Run("bad files", func(t *testing.T) { - err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")}) + err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")}, model.UTF8Validation) require.ErrorContains(t, err, "field unexpected_field not found in type rulefmt.Rule") }) } @@ -2666,10 +2689,11 @@ func TestRuleDependencyController_AnalyseRules(t *testing.T) { t.Cleanup(func() { storage.Close() }) ruleManager := NewManager(&ManagerOptions{ - Context: context.Background(), - Logger: promslog.NewNopLogger(), - Appendable: storage, - QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + Appendable: storage, + NameValidationScheme: model.UTF8Validation, + QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, }) groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, tc.ruleFile) @@ -2695,10 +2719,11 @@ func BenchmarkRuleDependencyController_AnalyseRules(b *testing.B) { b.Cleanup(func() { storage.Close() }) ruleManager := NewManager(&ManagerOptions{ - Context: context.Background(), - Logger: promslog.NewNopLogger(), - Appendable: storage, - QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, + NameValidationScheme: model.UTF8Validation, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + Appendable: storage, + QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, }) groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, "fixtures/rules_multiple.yaml") diff --git a/scrape/manager_test.go b/scrape/manager_test.go index df19dd5a83..ed30a7ce92 100644 --- a/scrape/manager_test.go +++ b/scrape/manager_test.go @@ -188,11 +188,12 @@ func TestPopulateLabels(t *testing.T) { ScrapeTimeout: model.Duration(time.Second), RelabelConfigs: []*relabel.Config{ { - Action: relabel.Replace, - Regex: relabel.MustNewRegexp("(.*)"), - SourceLabels: model.LabelNames{"custom"}, - Replacement: "${1}", - TargetLabel: string(model.AddressLabel), + Action: relabel.Replace, + Regex: relabel.MustNewRegexp("(.*)"), + SourceLabels: model.LabelNames{"custom"}, + Replacement: "${1}", + TargetLabel: string(model.AddressLabel), + NameValidationScheme: model.UTF8Validation, }, }, }, @@ -226,11 +227,12 @@ func TestPopulateLabels(t *testing.T) { ScrapeTimeout: model.Duration(time.Second), RelabelConfigs: []*relabel.Config{ { - Action: relabel.Replace, - Regex: relabel.MustNewRegexp("(.*)"), - SourceLabels: model.LabelNames{"custom"}, - Replacement: "${1}", - TargetLabel: string(model.AddressLabel), + Action: relabel.Replace, + Regex: relabel.MustNewRegexp("(.*)"), + SourceLabels: model.LabelNames{"custom"}, + Replacement: "${1}", + TargetLabel: string(model.AddressLabel), + NameValidationScheme: model.UTF8Validation, }, }, }, @@ -450,6 +452,10 @@ func TestPopulateLabels(t *testing.T) { for _, c := range cases { in := maps.Clone(c.in) lb := labels.NewBuilder(labels.EmptyLabels()) + c.cfg.MetricNameValidationScheme = model.UTF8Validation + for i := range c.cfg.RelabelConfigs { + c.cfg.RelabelConfigs[i].NameValidationScheme = model.UTF8Validation + } res, err := PopulateLabels(lb, c.cfg, c.in, nil) if c.err != "" { require.EqualError(t, err, c.err) diff --git a/scrape/scrape.go b/scrape/scrape.go index 84e00af600..d2f344ea72 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -154,6 +154,11 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed return nil, err } + switch cfg.MetricNameValidationScheme { + case model.LegacyValidation, model.UTF8Validation: + default: + return nil, errors.New("newScrapePool: MetricNameValidationScheme must be set in scrape configuration") + } var escapingScheme model.EscapingScheme escapingScheme, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme) if err != nil { @@ -326,6 +331,11 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { sp.config = cfg oldClient := sp.client sp.client = client + switch cfg.MetricNameValidationScheme { + case model.LegacyValidation, model.UTF8Validation: + default: + return errors.New("scrapePool.reload: MetricNameValidationScheme must be set in scrape configuration") + } sp.validationScheme = cfg.MetricNameValidationScheme var escapingScheme model.EscapingScheme escapingScheme, err = model.ToEscapingScheme(cfg.MetricNameEscapingScheme) diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index c226be2d33..3033c1a1a6 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -334,9 +334,10 @@ func TestDroppedTargetsList(t *testing.T) { MetricNameEscapingScheme: model.AllowUTF8, RelabelConfigs: []*relabel.Config{ { - Action: relabel.Drop, - Regex: relabel.MustNewRegexp("dropMe"), - SourceLabels: model.LabelNames{"job"}, + Action: relabel.Drop, + Regex: relabel.MustNewRegexp("dropMe"), + SourceLabels: model.LabelNames{"job"}, + NameValidationScheme: model.UTF8Validation, }, }, } @@ -1367,10 +1368,11 @@ func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) { labels: labels.FromStrings("pod_label_invalid_012\xff", "test"), } relabelConfig := []*relabel.Config{{ - Action: relabel.LabelMap, - Regex: relabel.MustNewRegexp("pod_label_invalid_(.+)"), - Separator: ";", - Replacement: "$1", + Action: relabel.LabelMap, + Regex: relabel.MustNewRegexp("pod_label_invalid_(.+)"), + Separator: ";", + Replacement: "$1", + NameValidationScheme: model.UTF8Validation, }} sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0) sl.sampleMutator = func(l labels.Labels) labels.Labels { @@ -4182,18 +4184,20 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) { MetricNameEscapingScheme: model.AllowUTF8, RelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{model.ScrapeIntervalLabel}, - Regex: relabel.MustNewRegexp("2s"), - Replacement: "3s", - TargetLabel: model.ScrapeIntervalLabel, - Action: relabel.Replace, + SourceLabels: model.LabelNames{model.ScrapeIntervalLabel}, + Regex: relabel.MustNewRegexp("2s"), + Replacement: "3s", + TargetLabel: model.ScrapeIntervalLabel, + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{model.ScrapeTimeoutLabel}, - Regex: relabel.MustNewRegexp("500ms"), - Replacement: "750ms", - TargetLabel: model.ScrapeTimeoutLabel, - Action: relabel.Replace, + SourceLabels: model.LabelNames{model.ScrapeTimeoutLabel}, + Regex: relabel.MustNewRegexp("500ms"), + Replacement: "750ms", + TargetLabel: model.ScrapeTimeoutLabel, + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, }, } @@ -4220,20 +4224,22 @@ func TestLeQuantileReLabel(t *testing.T) { JobName: "test", MetricRelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"le", "__name__"}, - Regex: relabel.MustNewRegexp("(\\d+)\\.0+;.*_bucket"), - Replacement: relabel.DefaultRelabelConfig.Replacement, - Separator: relabel.DefaultRelabelConfig.Separator, - TargetLabel: "le", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"le", "__name__"}, + Regex: relabel.MustNewRegexp("(\\d+)\\.0+;.*_bucket"), + Replacement: relabel.DefaultRelabelConfig.Replacement, + Separator: relabel.DefaultRelabelConfig.Separator, + TargetLabel: "le", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"quantile"}, - Regex: relabel.MustNewRegexp("(\\d+)\\.0+"), - Replacement: relabel.DefaultRelabelConfig.Replacement, - Separator: relabel.DefaultRelabelConfig.Separator, - TargetLabel: "quantile", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"quantile"}, + Regex: relabel.MustNewRegexp("(\\d+)\\.0+"), + Replacement: relabel.DefaultRelabelConfig.Replacement, + Separator: relabel.DefaultRelabelConfig.Separator, + TargetLabel: "quantile", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, }, SampleLimit: 100, @@ -4862,18 +4868,20 @@ func TestTypeUnitReLabel(t *testing.T) { JobName: "test", MetricRelabelConfigs: []*relabel.Config{ { - SourceLabels: model.LabelNames{"__name__"}, - Regex: relabel.MustNewRegexp(".*_total$"), - Replacement: "counter", - TargetLabel: "__type__", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"__name__"}, + Regex: relabel.MustNewRegexp(".*_total$"), + Replacement: "counter", + TargetLabel: "__type__", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, { - SourceLabels: model.LabelNames{"__name__"}, - Regex: relabel.MustNewRegexp(".*_bytes$"), - Replacement: "bytes", - TargetLabel: "__unit__", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"__name__"}, + Regex: relabel.MustNewRegexp(".*_bytes$"), + Replacement: "bytes", + TargetLabel: "__unit__", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }, }, SampleLimit: 100, @@ -5482,25 +5490,28 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) { Params: url.Values{"param": []string{secondParam}}, RelabelConfigs: []*relabel.Config{ { - Action: relabel.DefaultRelabelConfig.Action, - Regex: relabel.DefaultRelabelConfig.Regex, - SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, - TargetLabel: model.ScrapeTimeoutLabel, - Replacement: expectedTimeoutLabel, + Action: relabel.DefaultRelabelConfig.Action, + Regex: relabel.DefaultRelabelConfig.Regex, + SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, + TargetLabel: model.ScrapeTimeoutLabel, + Replacement: expectedTimeoutLabel, + NameValidationScheme: model.UTF8Validation, }, { - Action: relabel.DefaultRelabelConfig.Action, - Regex: relabel.DefaultRelabelConfig.Regex, - SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, - TargetLabel: paramLabel, - Replacement: expectedParam, + Action: relabel.DefaultRelabelConfig.Action, + Regex: relabel.DefaultRelabelConfig.Regex, + SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, + TargetLabel: paramLabel, + Replacement: expectedParam, + NameValidationScheme: model.UTF8Validation, }, { - Action: relabel.DefaultRelabelConfig.Action, - Regex: relabel.DefaultRelabelConfig.Regex, - SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, - TargetLabel: model.MetricsPathLabel, - Replacement: expectedPath, + Action: relabel.DefaultRelabelConfig.Action, + Regex: relabel.DefaultRelabelConfig.Regex, + SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, + TargetLabel: model.MetricsPathLabel, + Replacement: expectedPath, + NameValidationScheme: model.UTF8Validation, }, }, }, diff --git a/storage/remote/codec.go b/storage/remote/codec.go index 3dbf432bcf..08139a5974 100644 --- a/storage/remote/codec.go +++ b/storage/remote/codec.go @@ -766,10 +766,10 @@ func (it *chunkedSeriesIterator) Err() error { // also making sure that there are no labels with duplicate names. func validateLabelsAndMetricName(ls []prompb.Label) error { for i, l := range ls { - if l.Name == labels.MetricName && !model.IsValidMetricName(model.LabelValue(l.Value)) { + if l.Name == labels.MetricName && !model.UTF8Validation.IsValidMetricName(l.Value) { return fmt.Errorf("invalid metric name: %v", l.Value) } - if !model.LabelName(l.Name).IsValid() { + if !model.UTF8Validation.IsValidLabelName(l.Name) { return fmt.Errorf("invalid label name: %v", l.Name) } if !model.LabelValue(l.Value).IsValid() { diff --git a/storage/remote/queue_manager_test.go b/storage/remote/queue_manager_test.go index 3a75a64940..294069eb83 100644 --- a/storage/remote/queue_manager_test.go +++ b/storage/remote/queue_manager_test.go @@ -1418,12 +1418,13 @@ func BenchmarkStoreSeries(b *testing.B) { {Name: "replica", Value: "1"}, } relabelConfigs := []*relabel.Config{{ - SourceLabels: model.LabelNames{"namespace"}, - Separator: ";", - Regex: relabel.MustNewRegexp("kube.*"), - TargetLabel: "job", - Replacement: "$1", - Action: relabel.Replace, + SourceLabels: model.LabelNames{"namespace"}, + Separator: ";", + Regex: relabel.MustNewRegexp("kube.*"), + TargetLabel: "job", + Replacement: "$1", + Action: relabel.Replace, + NameValidationScheme: model.UTF8Validation, }} testCases := []struct { name string diff --git a/storage/remote/write_test.go b/storage/remote/write_test.go index 9c347ae8e7..038f86abf0 100644 --- a/storage/remote/write_test.go +++ b/storage/remote/write_test.go @@ -282,7 +282,8 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) { QueueConfig: config.DefaultQueueConfig, WriteRelabelConfigs: []*relabel.Config{ { - Regex: relabel.MustNewRegexp(".+"), + Regex: relabel.MustNewRegexp(".+"), + NameValidationScheme: model.UTF8Validation, }, }, ProtobufMessage: config.RemoteWriteProtoMsgV1, @@ -329,7 +330,10 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) { storeHashes() // Update c0 and c2. - c0.WriteRelabelConfigs[0] = &relabel.Config{Regex: relabel.MustNewRegexp("foo")} + c0.WriteRelabelConfigs[0] = &relabel.Config{ + Regex: relabel.MustNewRegexp("foo"), + NameValidationScheme: model.UTF8Validation, + } c2.RemoteTimeout = model.Duration(50 * time.Second) conf = &config.Config{ GlobalConfig: config.GlobalConfig{}, diff --git a/web/api/v1/api.go b/web/api/v1/api.go index a67c0aa525..adb01b16db 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -790,8 +790,7 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) { name = model.UnescapeName(name, model.ValueEncodingEscaping) } - label := model.LabelName(name) - if !label.IsValid() { + if !model.UTF8Validation.IsValidLabelName(name) { return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil} } diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 2c9d2d978b..078ff6f2ed 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -311,11 +311,12 @@ func (m *rulesRetrieverMock) CreateRuleGroups() { } engine := promqltest.NewTestEngineWithOpts(m.testing, engineOpts) opts := &rules.ManagerOptions{ - QueryFunc: rules.EngineQueryFunc(engine, storage), - Appendable: storage, - Context: context.Background(), - Logger: promslog.NewNopLogger(), - NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {}, + NameValidationScheme: model.UTF8Validation, + QueryFunc: rules.EngineQueryFunc(engine, storage), + Appendable: storage, + Context: context.Background(), + Logger: promslog.NewNopLogger(), + NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {}, } var r []rules.Rule