This commit is contained in:
Arve Knudsen 2025-08-05 12:38:54 +02:00 committed by GitHub
commit 8d0d51dce4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 685 additions and 444 deletions

View File

@ -651,7 +651,7 @@ func main() {
} }
// Parse rule files to verify they exist and contain valid rules. // 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) absPath, pathErr := filepath.Abs(cfg.configFile)
if pathErr != nil { if pathErr != nil {
absPath = cfg.configFile absPath = cfg.configFile
@ -790,7 +790,7 @@ func main() {
ctxWeb, cancelWeb = context.WithCancel(context.Background()) ctxWeb, cancelWeb = context.WithCancel(context.Background())
ctxRule = 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()) ctxScrape, cancelScrape = context.WithCancel(context.Background())
ctxNotify, cancelNotify = context.WithCancel(context.Background()) ctxNotify, cancelNotify = context.WithCancel(context.Background())
@ -867,6 +867,7 @@ func main() {
queryEngine = promql.NewEngine(opts) queryEngine = promql.NewEngine(opts)
ruleManager = rules.NewManager(&rules.ManagerOptions{ ruleManager = rules.NewManager(&rules.ManagerOptions{
NameValidationScheme: cfgFile.GlobalConfig.MetricNameValidationScheme,
Appendable: fanoutStorage, Appendable: fanoutStorage,
Queryable: localStorage, Queryable: localStorage,
QueryFunc: rules.EngineQueryFunc(queryEngine, fanoutStorage), QueryFunc: rules.EngineQueryFunc(queryEngine, fanoutStorage),

View File

@ -359,7 +359,7 @@ func main() {
os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, prometheus.DefaultRegisterer)) os.Exit(CheckSD(*sdConfigFile, *sdJobName, *sdTimeout, prometheus.DefaultRegisterer))
case checkConfigCmd.FullCommand(): 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(): case checkServerHealthCmd.FullCommand():
os.Exit(checkErr(CheckServerStatus(serverURL, checkHealth, httpRoundTripper))) os.Exit(checkErr(CheckServerStatus(serverURL, checkHealth, httpRoundTripper)))
@ -371,7 +371,7 @@ func main() {
os.Exit(CheckWebConfig(*webConfigFiles...)) os.Exit(CheckWebConfig(*webConfigFiles...))
case checkRulesCmd.FullCommand(): case checkRulesCmd.FullCommand():
os.Exit(CheckRules(newRulesLintConfig(*checkRulesLint, *checkRulesLintFatal, *checkRulesIgnoreUnknownFields), *ruleFiles...)) os.Exit(CheckRules(newRulesLintConfig(*checkRulesLint, *checkRulesLintFatal, *checkRulesIgnoreUnknownFields, model.UTF8Validation), *ruleFiles...))
case checkMetricsCmd.FullCommand(): case checkMetricsCmd.FullCommand():
os.Exit(CheckMetrics(*checkMetricsExtended)) os.Exit(CheckMetrics(*checkMetricsExtended))
@ -436,7 +436,7 @@ func main() {
os.Exit(backfillOpenMetrics(*importFilePath, *importDBPath, *importHumanReadable, *importQuiet, *maxBlockDuration, *openMetricsLabels)) os.Exit(backfillOpenMetrics(*importFilePath, *importDBPath, *importHumanReadable, *importQuiet, *maxBlockDuration, *openMetricsLabels))
case importRulesCmd.FullCommand(): 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(): case queryAnalyzeCmd.FullCommand():
os.Exit(checkErr(queryAnalyzeCfg.run(serverURL, httpRoundTripper))) os.Exit(checkErr(queryAnalyzeCfg.run(serverURL, httpRoundTripper)))
@ -468,17 +468,19 @@ func checkExperimental(f bool) {
var errLint = errors.New("lint error") var errLint = errors.New("lint error")
type rulesLintConfig struct { type rulesLintConfig struct {
all bool all bool
duplicateRules bool duplicateRules bool
fatal bool fatal bool
ignoreUnknownFields 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, ",") items := strings.Split(stringVal, ",")
ls := rulesLintConfig{ ls := rulesLintConfig{
fatal: fatal, fatal: fatal,
ignoreUnknownFields: ignoreUnknownFields, ignoreUnknownFields: ignoreUnknownFields,
nameValidationScheme: nameValidationScheme,
} }
for _, setting := range items { for _, setting := range items {
switch setting { switch setting {
@ -504,7 +506,7 @@ type configLintConfig struct {
lookbackDelta model.Duration 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{ c := configLintConfig{
rulesLintConfig: rulesLintConfig{ rulesLintConfig: rulesLintConfig{
fatal: fatal, fatal: fatal,
@ -533,7 +535,7 @@ func newConfigLintConfig(optionsStr string, fatal, ignoreUnknownFields bool, loo
} }
if len(rulesOptions) > 0 { if len(rulesOptions) > 0 {
c.rulesLintConfig = newRulesLintConfig(strings.Join(rulesOptions, ","), fatal, ignoreUnknownFields) c.rulesLintConfig = newRulesLintConfig(strings.Join(rulesOptions, ","), fatal, ignoreUnknownFields, nameValidationScheme)
} }
return c return c
@ -854,7 +856,7 @@ func checkRulesFromStdin(ls rulesLintConfig) (bool, bool) {
fmt.Fprintln(os.Stderr, " FAILED:", err) fmt.Fprintln(os.Stderr, " FAILED:", err)
return true, true return true, true
} }
rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields) rgs, errs := rulefmt.Parse(data, ls.ignoreUnknownFields, ls.nameValidationScheme)
if errs != nil { if errs != nil {
failed = true failed = true
fmt.Fprintln(os.Stderr, " FAILED:") fmt.Fprintln(os.Stderr, " FAILED:")
@ -888,7 +890,7 @@ func checkRules(files []string, ls rulesLintConfig) (bool, bool) {
hasErrors := false hasErrors := false
for _, f := range files { for _, f := range files {
fmt.Println("Checking", f) fmt.Println("Checking", f)
rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields) rgs, errs := rulefmt.ParseFile(f, ls.ignoreUnknownFields, ls.nameValidationScheme)
if errs != nil { if errs != nil {
failed = true failed = true
fmt.Fprintln(os.Stderr, " FAILED:") 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 // importRules backfills recording rules from the files provided. The output are blocks of data
// at the outputDir location. // 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() ctx := context.Background()
var stime, etime time.Time var stime, etime time.Time
var err error var err error
@ -1248,11 +1250,12 @@ func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outpu
} }
cfg := ruleImporterConfig{ cfg := ruleImporterConfig{
outputDir: outputDir, outputDir: outputDir,
start: stime, start: stime,
end: etime, end: etime,
evalInterval: evalInterval, evalInterval: evalInterval,
maxBlockDuration: maxBlockDuration, maxBlockDuration: maxBlockDuration,
nameValidationScheme: nameValidationScheme,
} }
api, err := newAPI(url, roundTripper, nil) api, err := newAPI(url, roundTripper, nil)
if err != nil { if err != nil {

View File

@ -186,7 +186,7 @@ func TestCheckDuplicates(t *testing.T) {
c := test c := test
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
t.Parallel() t.Parallel()
rgs, err := rulefmt.ParseFile(c.ruleFile, false) rgs, err := rulefmt.ParseFile(c.ruleFile, false, model.UTF8Validation)
require.Empty(t, err) require.Empty(t, err)
dups := checkDuplicates(rgs.Groups) dups := checkDuplicates(rgs.Groups)
require.Equal(t, c.expectedDups, dups) require.Equal(t, c.expectedDups, dups)
@ -195,7 +195,7 @@ func TestCheckDuplicates(t *testing.T) {
} }
func BenchmarkCheckDuplicates(b *testing.B) { 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) require.Empty(b, err)
b.ResetTimer() b.ResetTimer()
@ -509,7 +509,7 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin) defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r os.Stdin = r
exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false)) exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation))
require.Equal(t, successExitCode, exitCode) require.Equal(t, successExitCode, exitCode)
}) })
@ -531,7 +531,7 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin) defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r os.Stdin = r
exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false)) exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, false, false, model.UTF8Validation))
require.Equal(t, failureExitCode, exitCode) require.Equal(t, failureExitCode, exitCode)
}) })
@ -553,7 +553,7 @@ func TestCheckRules(t *testing.T) {
defer func(v *os.File) { os.Stdin = v }(os.Stdin) defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r os.Stdin = r
exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false)) exitCode := CheckRules(newRulesLintConfig(lintOptionDuplicateRules, true, false, model.UTF8Validation))
require.Equal(t, lintErrExitCode, exitCode) require.Equal(t, lintErrExitCode, exitCode)
}) })
} }
@ -571,19 +571,19 @@ func TestCheckRulesWithFeatureFlag(t *testing.T) {
func TestCheckRulesWithRuleFiles(t *testing.T) { func TestCheckRulesWithRuleFiles(t *testing.T) {
t.Run("rules-good", func(t *testing.T) { t.Run("rules-good", func(t *testing.T) {
t.Parallel() 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) require.Equal(t, successExitCode, exitCode)
}) })
t.Run("rules-bad", func(t *testing.T) { t.Run("rules-bad", func(t *testing.T) {
t.Parallel() 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) require.Equal(t, failureExitCode, exitCode)
}) })
t.Run("rules-lint-fatal", func(t *testing.T) { t.Run("rules-lint-fatal", func(t *testing.T) {
t.Parallel() 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) require.Equal(t, lintErrExitCode, exitCode)
}) })
} }
@ -612,20 +612,20 @@ func TestCheckScrapeConfigs(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Non-fatal linting. // 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") require.Equal(t, successExitCode, code, "Non-fatal linting should return success")
// Fatal linting. // 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 { if tc.expectError {
require.Equal(t, lintErrExitCode, code, "Fatal linting should return error") require.Equal(t, lintErrExitCode, code, "Fatal linting should return error")
} else { } else {
require.Equal(t, successExitCode, code, "Fatal linting should return success when there are no problems") require.Equal(t, successExitCode, code, "Fatal linting should return success when there are no problems")
} }
// Check syntax only, no linting. // 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") require.Equal(t, successExitCode, code, "Fatal linting should return success when checking syntax only")
// Lint option "none" should disable linting. // 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`) require.Equal(t, successExitCode, code, `Fatal linting should return success when lint option "none" is specified`)
}) })
} }

View File

@ -48,11 +48,12 @@ type ruleImporter struct {
} }
type ruleImporterConfig struct { type ruleImporterConfig struct {
outputDir string outputDir string
start time.Time start time.Time
end time.Time end time.Time
evalInterval time.Duration evalInterval time.Duration
maxBlockDuration 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 // 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 { 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)) logger.Info("new rule importer", "component", "backfiller", "start", config.start.Format(time.RFC822), "end", config.end.Format(time.RFC822))
return &ruleImporter{ return &ruleImporter{
logger: logger, logger: logger,
config: config, config: config,
apiClient: apiClient, apiClient: apiClient,
ruleManager: rules.NewManager(&rules.ManagerOptions{}), ruleManager: rules.NewManager(&rules.ManagerOptions{
NameValidationScheme: config.nameValidationScheme,
}),
} }
} }

View File

@ -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) { func newTestRuleImporter(_ context.Context, start time.Time, tmpDir string, testSamples model.Matrix, maxBlockDuration time.Duration) (*ruleImporter, error) {
logger := promslog.NewNopLogger() logger := promslog.NewNopLogger()
cfg := ruleImporterConfig{ cfg := ruleImporterConfig{
outputDir: tmpDir, outputDir: tmpDir,
start: start.Add(-10 * time.Hour), start: start.Add(-10 * time.Hour),
end: start.Add(-7 * time.Hour), end: start.Add(-7 * time.Hour),
evalInterval: 60 * time.Second, evalInterval: 60 * time.Second,
maxBlockDuration: maxBlockDuration, maxBlockDuration: maxBlockDuration,
nameValidationScheme: model.UTF8Validation,
} }
return newRuleImporter(logger, cfg, mockQueryRangeAPI{ return newRuleImporter(logger, cfg, mockQueryRangeAPI{

View File

@ -42,11 +42,12 @@ func TestSDCheckResult(t *testing.T) {
ScrapeInterval: model.Duration(1 * time.Minute), ScrapeInterval: model.Duration(1 * time.Minute),
ScrapeTimeout: model.Duration(10 * time.Second), ScrapeTimeout: model.Duration(10 * time.Second),
RelabelConfigs: []*relabel.Config{{ RelabelConfigs: []*relabel.Config{{
SourceLabels: model.LabelNames{"foo"}, SourceLabels: model.LabelNames{"foo"},
Action: relabel.Replace, Action: relabel.Replace,
TargetLabel: "newfoo", TargetLabel: "newfoo",
Regex: reg, Regex: reg,
Replacement: "$1", Replacement: "$1",
NameValidationScheme: model.UTF8Validation,
}}, }},
} }

View File

@ -222,11 +222,12 @@ func (tg *testGroup) test(testname string, evalInterval time.Duration, groupOrde
// Load the rule files. // Load the rule files.
opts := &rules.ManagerOptions{ opts := &rules.ManagerOptions{
QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()), QueryFunc: rules.EngineQueryFunc(suite.QueryEngine(), suite.Storage()),
Appendable: suite.Storage(), Appendable: suite.Storage(),
Context: context.Background(), Context: context.Background(),
NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {}, NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {},
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
m := rules.NewManager(opts) m := rules.NewManager(opts)
groupsMap, ers := m.LoadGroups(time.Duration(tg.Interval), tg.ExternalLabels, tg.ExternalURL, nil, ignoreUnknownFields, ruleFiles...) groupsMap, ers := m.LoadGroups(time.Duration(tg.Interval), tg.ExternalLabels, tg.ExternalURL, nil, ignoreUnknownFields, ruleFiles...)

View File

@ -413,6 +413,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
jobNames[scfg.JobName] = struct{}{} jobNames[scfg.JobName] = struct{}{}
} }
if err := c.AlertingConfig.Validate(c.GlobalConfig.MetricNameValidationScheme); err != nil {
return err
}
rwNames := map[string]struct{}{} rwNames := map[string]struct{}{}
for _, rwcfg := range c.RemoteWriteConfigs { for _, rwcfg := range c.RemoteWriteConfigs {
if rwcfg == nil { if rwcfg == nil {
@ -422,6 +426,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if _, ok := rwNames[rwcfg.Name]; ok && rwcfg.Name != "" { if _, ok := rwNames[rwcfg.Name]; ok && rwcfg.Name != "" {
return fmt.Errorf("found multiple remote write configs with job name %q", 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{}{} rwNames[rwcfg.Name] = struct{}{}
} }
rrNames := map[string]struct{}{} rrNames := map[string]struct{}{}
@ -596,8 +603,14 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
return err 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 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) return fmt.Errorf("%q is not a valid label name", l.Name)
} }
if !model.LabelValue(l.Value).IsValid() { if !model.LabelValue(l.Value).IsValid() {
@ -878,11 +891,9 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error {
} }
switch globalConfig.MetricNameValidationScheme { switch globalConfig.MetricNameValidationScheme {
case model.UnsetValidation:
globalConfig.MetricNameValidationScheme = model.UTF8Validation
case model.LegacyValidation, model.UTF8Validation: case model.LegacyValidation, model.UTF8Validation:
default: 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. // Scrapeconfig validation scheme matches global if left blank.
localValidationUnset := false localValidationUnset := false
@ -944,6 +955,17 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error {
c.AlwaysScrapeClassicHistograms = &global 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 return nil
} }
@ -1096,6 +1118,20 @@ type AlertingConfig struct {
AlertmanagerConfigs AlertmanagerConfigs `yaml:"alertmanagers,omitempty"` 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. // SetDirectory joins any relative file paths with dir.
func (c *AlertingConfig) SetDirectory(dir string) { func (c *AlertingConfig) SetDirectory(dir string) {
for _, c := range c.AlertmanagerConfigs { for _, c := range c.AlertmanagerConfigs {
@ -1240,6 +1276,20 @@ func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) er
return nil 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. // MarshalYAML implements the yaml.Marshaler interface.
func (c *AlertmanagerConfig) MarshalYAML() (interface{}, error) { func (c *AlertmanagerConfig) MarshalYAML() (interface{}, error) {
return discovery.MarshalYAMLWithInlineConfigs(c) return discovery.MarshalYAMLWithInlineConfigs(c)
@ -1377,6 +1427,16 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err
return validateAuthConfigs(c) 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. // validateAuthConfigs validates that at most one of basic_auth, authorization, oauth2, sigv4, azuread or google_iam must be configured.
func validateAuthConfigs(c *RemoteWriteConfig) error { func validateAuthConfigs(c *RemoteWriteConfig) error {
var authConfigured []string var authConfigured []string

View File

@ -23,6 +23,8 @@ import (
"time" "time"
"github.com/alecthomas/units" "github.com/alecthomas/units"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/regexp" "github.com/grafana/regexp"
"github.com/prometheus/common/config" "github.com/prometheus/common/config"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
@ -106,6 +108,7 @@ var expectedConf = &Config{
ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols, ScrapeProtocols: DefaultGlobalConfig.ScrapeProtocols,
AlwaysScrapeClassicHistograms: false, AlwaysScrapeClassicHistograms: false,
ConvertClassicHistogramsToNHCB: false, ConvertClassicHistogramsToNHCB: false,
MetricNameValidationScheme: model.UTF8Validation,
}, },
Runtime: RuntimeConfig{ Runtime: RuntimeConfig{
@ -125,11 +128,12 @@ var expectedConf = &Config{
Name: "drop_expensive", Name: "drop_expensive",
WriteRelabelConfigs: []*relabel.Config{ WriteRelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"__name__"}, SourceLabels: model.LabelNames{"__name__"},
Separator: ";", Separator: ";",
Regex: relabel.MustNewRegexp("expensive.*"), Regex: relabel.MustNewRegexp("expensive.*"),
Replacement: "$1", Replacement: "$1",
Action: relabel.Drop, Action: relabel.Drop,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
QueueConfig: DefaultQueueConfig, QueueConfig: DefaultQueueConfig,
@ -279,50 +283,56 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"job", "__meta_dns_name"}, SourceLabels: model.LabelNames{"job", "__meta_dns_name"},
TargetLabel: "job", TargetLabel: "job",
Separator: ";", Separator: ";",
Regex: relabel.MustNewRegexp("(.*)some-[regex]"), Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
Replacement: "foo-${1}", Replacement: "foo-${1}",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"abc"}, SourceLabels: model.LabelNames{"abc"},
TargetLabel: "cde", TargetLabel: "cde",
Separator: ";", Separator: ";",
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
TargetLabel: "abc", TargetLabel: "abc",
Separator: ";", Separator: ";",
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
Replacement: "static", Replacement: "static",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
TargetLabel: "abc", TargetLabel: "abc",
Separator: ";", Separator: ";",
Regex: relabel.MustNewRegexp(""), Regex: relabel.MustNewRegexp(""),
Replacement: "static", Replacement: "static",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"foo"}, SourceLabels: model.LabelNames{"foo"},
TargetLabel: "abc", TargetLabel: "abc",
Action: relabel.KeepEqual, Action: relabel.KeepEqual,
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Separator: relabel.DefaultRelabelConfig.Separator, Separator: relabel.DefaultRelabelConfig.Separator,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"foo"}, SourceLabels: model.LabelNames{"foo"},
TargetLabel: "abc", TargetLabel: "abc",
Action: relabel.DropEqual, Action: relabel.DropEqual,
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Separator: relabel.DefaultRelabelConfig.Separator, Separator: relabel.DefaultRelabelConfig.Separator,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, },
@ -377,54 +387,61 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"job"}, SourceLabels: model.LabelNames{"job"},
Regex: relabel.MustNewRegexp("(.*)some-[regex]"), Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
Separator: ";", Separator: ";",
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.Drop, Action: relabel.Drop,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"__address__"}, SourceLabels: model.LabelNames{"__address__"},
TargetLabel: "__tmp_hash", TargetLabel: "__tmp_hash",
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Modulus: 8, Modulus: 8,
Separator: ";", Separator: ";",
Action: relabel.HashMod, Action: relabel.HashMod,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"__tmp_hash"}, SourceLabels: model.LabelNames{"__tmp_hash"},
Regex: relabel.MustNewRegexp("1"), Regex: relabel.MustNewRegexp("1"),
Separator: ";", Separator: ";",
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.Keep, Action: relabel.Keep,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
Regex: relabel.MustNewRegexp("1"), Regex: relabel.MustNewRegexp("1"),
Separator: ";", Separator: ";",
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.LabelMap, Action: relabel.LabelMap,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
Regex: relabel.MustNewRegexp("d"), Regex: relabel.MustNewRegexp("d"),
Separator: ";", Separator: ";",
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.LabelDrop, Action: relabel.LabelDrop,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
Regex: relabel.MustNewRegexp("k"), Regex: relabel.MustNewRegexp("k"),
Separator: ";", Separator: ";",
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.LabelKeep, Action: relabel.LabelKeep,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
MetricRelabelConfigs: []*relabel.Config{ MetricRelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"__name__"}, SourceLabels: model.LabelNames{"__name__"},
Regex: relabel.MustNewRegexp("expensive_metric.*"), Regex: relabel.MustNewRegexp("expensive_metric.*"),
Separator: ";", Separator: ";",
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Action: relabel.Drop, Action: relabel.Drop,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, },
@ -479,12 +496,13 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"__meta_sd_consul_tags"}, SourceLabels: model.LabelNames{"__meta_sd_consul_tags"},
Regex: relabel.MustNewRegexp("label:([^=]+)=([^,]+)"), Regex: relabel.MustNewRegexp("label:([^=]+)=([^,]+)"),
Separator: ",", Separator: ",",
TargetLabel: "${1}", TargetLabel: "${1}",
Replacement: "${2}", Replacement: "${2}",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, },
@ -1276,12 +1294,13 @@ var expectedConf = &Config{
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
Action: relabel.Uppercase, Action: relabel.Uppercase,
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Separator: relabel.DefaultRelabelConfig.Separator, Separator: relabel.DefaultRelabelConfig.Separator,
SourceLabels: model.LabelNames{"instance"}, SourceLabels: model.LabelNames{"instance"},
TargetLabel: "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()) c, err := LoadFile("testdata/conf.good.yml", false, promslog.NewNopLogger())
require.NoError(t, err) 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) { func TestScrapeIntervalLarger(t *testing.T) {

2
go.mod
View File

@ -50,7 +50,7 @@ require (
github.com/prometheus/alertmanager v0.28.1 github.com/prometheus/alertmanager v0.28.1
github.com/prometheus/client_golang v1.23.0-rc.1 github.com/prometheus/client_golang v1.23.0-rc.1
github.com/prometheus/client_model v0.6.2 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/common/assets v0.2.0
github.com/prometheus/exporter-toolkit v0.14.0 github.com/prometheus/exporter-toolkit v0.14.0
github.com/prometheus/sigv4 v0.2.0 github.com/prometheus/sigv4 v0.2.0

4
go.sum
View File

@ -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/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.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 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.20250801071412-c79a891c6c28 h1:9CaJtf5ZS3GQVCVoslEkJcKSVwiD9aTqwgMpG1n9zQw=
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/go.mod h1:LL3lcZII3UXGO4InbF+BTSsiAAPUBnwFVbp4gBWIMqw=
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM= 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/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=

View File

@ -53,7 +53,7 @@ func (ls Labels) String() string {
b.WriteByte(',') b.WriteByte(',')
b.WriteByte(' ') b.WriteByte(' ')
} }
if !model.LabelName(l.Name).IsValidLegacy() { if !model.LegacyValidation.IsValidLabelName(l.Name) {
b.Write(strconv.AppendQuote(b.AvailableBuffer(), l.Name)) b.Write(strconv.AppendQuote(b.AvailableBuffer(), l.Name))
} else { } else {
b.WriteString(l.Name) b.WriteString(l.Name)
@ -106,18 +106,11 @@ func (ls Labels) IsValid(validationScheme model.ValidationScheme) bool {
if l.Name == model.MetricNameLabel { if l.Name == model.MetricNameLabel {
// If the default validation scheme has been overridden with legacy mode, // If the default validation scheme has been overridden with legacy mode,
// we need to call the special legacy validation checker. // we need to call the special legacy validation checker.
if validationScheme == model.LegacyValidation && !model.IsValidLegacyMetricName(string(model.LabelValue(l.Value))) { if !validationScheme.IsValidMetricName(l.Value) {
return strconv.ErrSyntax
}
if !model.IsValidMetricName(model.LabelValue(l.Value)) {
return strconv.ErrSyntax return strconv.ErrSyntax
} }
} }
if validationScheme == model.LegacyValidation { if !validationScheme.IsValidLabelName(l.Name) || !model.LabelValue(l.Value).IsValid() {
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() {
return strconv.ErrSyntax return strconv.ErrSyntax
} }
return nil return nil

View File

@ -100,6 +100,8 @@ type Config struct {
Replacement string `yaml:"replacement,omitempty" json:"replacement,omitempty"` Replacement string `yaml:"replacement,omitempty" json:"replacement,omitempty"`
// Action is the action to be performed for the relabeling. // Action is the action to be performed for the relabeling.
Action Action `yaml:"action,omitempty" json:"action,omitempty"` 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. // UnmarshalYAML implements the yaml.Unmarshaler interface.
@ -112,10 +114,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if c.Regex.Regexp == nil { if c.Regex.Regexp == nil {
c.Regex = MustNewRegexp("") c.Regex = MustNewRegexp("")
} }
return c.Validate() return nil
} }
func (c *Config) Validate() error { func (c *Config) Validate(nameValidationScheme model.ValidationScheme) error {
if c.Action == "" { if c.Action == "" {
return errors.New("relabel action cannot be empty") 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 == "" { 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) 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) 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. // UTF-8 allows ${} characters, so standard validation allow $variables by default.
// TODO(bwplotka): Relabelling users cannot put $ and ${<...>} characters in metric names or values. // TODO(bwplotka): Relabelling users cannot put $ and ${<...>} characters in metric names or values.
// Design escaping mechanism to allow that, once valid use case appears. // 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) { 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) 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) 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 { 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) { if c.Action == LabelMap && !isValidLabelNameWithRegexVarFn(c.Replacement) {
return fmt.Errorf("%q is invalid 'replacement' for %s action", c.Replacement, c.Action) 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) 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 { if indexes == nil {
break break
} }
target := model.LabelName(cfg.Regex.ExpandString([]byte{}, cfg.TargetLabel, val, indexes)) target := string(cfg.Regex.ExpandString([]byte{}, cfg.TargetLabel, val, indexes))
if !target.IsValid() { if !cfg.NameValidationScheme.IsValidLabelName(target) {
break break
} }
res := cfg.Regex.ExpandString([]byte{}, cfg.Replacement, val, indexes) res := cfg.Regex.ExpandString([]byte{}, cfg.Replacement, val, indexes)
if len(res) == 0 { if len(res) == 0 {
lb.Del(string(target)) lb.Del(target)
break break
} }
lb.Set(string(target), string(res)) lb.Set(target, string(res))
case Lowercase: case Lowercase:
lb.Set(cfg.TargetLabel, strings.ToLower(val)) lb.Set(cfg.TargetLabel, strings.ToLower(val))
case Uppercase: case Uppercase:

View File

@ -747,7 +747,8 @@ func TestRelabel(t *testing.T) {
if cfg.Replacement == "" { if cfg.Replacement == "" {
cfg.Replacement = DefaultRelabelConfig.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...) res, keep := Process(test.input, test.relabel...)
@ -764,59 +765,76 @@ func TestRelabelValidate(t *testing.T) {
expected string expected string
}{ }{
{ {
config: Config{}, config: Config{
NameValidationScheme: model.UTF8Validation,
},
expected: `relabel action cannot be empty`, expected: `relabel action cannot be empty`,
}, },
{ {
config: Config{ config: Config{
Action: Replace, Action: Replace,
NameValidationScheme: model.UTF8Validation,
}, },
expected: `requires 'target_label' value`, expected: `requires 'target_label' value`,
}, },
{ {
config: Config{ config: Config{
Action: Lowercase, Action: Lowercase,
NameValidationScheme: model.UTF8Validation,
}, },
expected: `requires 'target_label' value`, expected: `requires 'target_label' value`,
}, },
{ {
config: Config{ config: Config{
Action: Lowercase, Action: Lowercase,
Replacement: DefaultRelabelConfig.Replacement, Replacement: DefaultRelabelConfig.Replacement,
TargetLabel: "${3}", // With UTF-8 naming, this is now a legal relabel rule. TargetLabel: "${3}", // With UTF-8 naming, this is now a legal relabel rule.
NameValidationScheme: model.UTF8Validation,
}, },
}, },
{ {
config: Config{ config: Config{
SourceLabels: model.LabelNames{"a"}, Action: Lowercase,
Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), Replacement: DefaultRelabelConfig.Replacement,
Action: Replace, TargetLabel: "${3}", // Fails with legacy validation
Replacement: "${1}", NameValidationScheme: model.LegacyValidation,
TargetLabel: "${3}", },
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{ config: Config{
SourceLabels: model.LabelNames{"a"}, SourceLabels: model.LabelNames{"a"},
Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), Regex: MustNewRegexp("some-([^-]+)-([^,]+)"),
Action: Replace, Action: Replace,
Replacement: "${1}", Replacement: "${1}",
TargetLabel: "0${3}", // With UTF-8 naming this targets a valid label. TargetLabel: "0${3}", // With UTF-8 naming this targets a valid label.
NameValidationScheme: model.UTF8Validation,
}, },
}, },
{ {
config: Config{ config: Config{
SourceLabels: model.LabelNames{"a"}, SourceLabels: model.LabelNames{"a"},
Regex: MustNewRegexp("some-([^-]+)-([^,]+)"), Regex: MustNewRegexp("some-([^-]+)-([^,]+)"),
Action: Replace, Action: Replace,
Replacement: "${1}", Replacement: "${1}",
TargetLabel: "-${3}", // With UTF-8 naming this targets a valid label. TargetLabel: "-${3}", // With UTF-8 naming this targets a valid label.
NameValidationScheme: model.UTF8Validation,
}, },
}, },
} }
for i, test := range tests { for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) {
err := test.config.Validate() err := test.config.Validate(model.UTF8Validation)
if test.expected == "" { if test.expected == "" {
require.NoError(t, err) require.NoError(t, err)
} else { } else {

View File

@ -96,7 +96,14 @@ type ruleGroups struct {
} }
// Validate validates all rules in the rule groups. // 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{}{} set := map[string]struct{}{}
for j, g := range g.Groups { for j, g := range g.Groups {
@ -112,7 +119,7 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) {
} }
for k, v := range g.Labels { for k, v := range g.Labels {
if !model.LabelName(k).IsValid() || k == model.MetricNameLabel { if !nameValidationScheme.IsValidLabelName(k) || k == model.MetricNameLabel {
errs = append( errs = append(
errs, fmt.Errorf("invalid label name: %s", k), errs, fmt.Errorf("invalid label name: %s", k),
) )
@ -128,7 +135,7 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) {
set[g.Name] = struct{}{} set[g.Name] = struct{}{}
for i, r := range g.Rules { 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 var ruleName string
if r.Alert != "" { if r.Alert != "" {
ruleName = r.Alert ruleName = r.Alert
@ -192,7 +199,7 @@ type RuleNode struct {
} }
// Validate the rule and return a list of encountered errors. // 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 != "" { if r.Record != "" && r.Alert != "" {
nodes = append(nodes, WrappedError{ nodes = append(nodes, WrappedError{
err: errors.New("only one of 'record' and 'alert' must be set"), 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, node: &node.Record,
}) })
} }
if !model.IsValidMetricName(model.LabelValue(r.Record)) { if !nameValidationScheme.IsValidMetricName(r.Record) {
nodes = append(nodes, WrappedError{ nodes = append(nodes, WrappedError{
err: fmt.Errorf("invalid recording rule name: %s", r.Record), err: fmt.Errorf("invalid recording rule name: %s", r.Record),
node: &node.Record, node: &node.Record,
@ -255,7 +262,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) {
} }
for k, v := range r.Labels { 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{ nodes = append(nodes, WrappedError{
err: fmt.Errorf("invalid label name: %s", k), 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 { for k := range r.Annotations {
if !model.LabelName(k).IsValid() { if !nameValidationScheme.IsValidLabelName(k) {
nodes = append(nodes, WrappedError{ nodes = append(nodes, WrappedError{
err: fmt.Errorf("invalid annotation name: %s", k), 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. // 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 ( var (
groups RuleGroups groups RuleGroups
node ruleGroups node ruleGroups
@ -358,16 +365,16 @@ func Parse(content []byte, ignoreUnknownFields bool) (*RuleGroups, []error) {
return nil, errs return nil, errs
} }
return &groups, groups.Validate(node) return &groups, groups.Validate(node, nameValidationScheme)
} }
// ParseFile reads and parses rules from a file. // 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) b, err := os.ReadFile(file)
if err != nil { if err != nil {
return nil, []error{fmt.Errorf("%s: %w", file, err)} return nil, []error{fmt.Errorf("%s: %w", file, err)}
} }
rgs, errs := Parse(b, ignoreUnknownFields) rgs, errs := Parse(b, ignoreUnknownFields, nameValidationScheme)
for i := range errs { for i := range errs {
errs[i] = fmt.Errorf("%s: %w", file, errs[i]) errs[i] = fmt.Errorf("%s: %w", file, errs[i])
} }

View File

@ -19,17 +19,20 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func TestParseFileSuccess(t *testing.T) { 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") 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") 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") require.Empty(t, errs, "unexpected errors parsing file")
} }
@ -38,7 +41,7 @@ func TestParseFileSuccessWithAliases(t *testing.T) {
/ /
sum without(instance) (rate(requests_total[5m])) 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") require.Empty(t, errs, "unexpected errors parsing file")
for _, rg := range rgs.Groups { for _, rg := range rgs.Groups {
require.Equal(t, "HighAlert", rg.Rules[0].Alert) require.Equal(t, "HighAlert", rg.Rules[0].Alert)
@ -62,8 +65,9 @@ sum without(instance) (rate(requests_total[5m]))
func TestParseFileFailure(t *testing.T) { func TestParseFileFailure(t *testing.T) {
for _, c := range []struct { for _, c := range []struct {
filename string filename string
errMsg string errMsg string
nameValidationScheme model.ValidationScheme
}{ }{
{ {
filename: "duplicate_grp.bad.yaml", filename: "duplicate_grp.bad.yaml",
@ -105,9 +109,17 @@ func TestParseFileFailure(t *testing.T) {
filename: "record_and_keep_firing_for.bad.yaml", filename: "record_and_keep_firing_for.bad.yaml",
errMsg: "invalid field 'keep_firing_for' in recording rule", 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) { 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.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) require.ErrorContainsf(t, errs[0], c.errMsg, "Expected error for %s.", c.filename)
}) })
@ -203,7 +215,7 @@ groups:
} }
for _, tst := range tests { 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) require.NotNil(t, rgs, "Rule parsing, rule=\n"+tst.ruleString)
passed := (tst.shouldPass && len(errs) == 0) || (!tst.shouldPass && len(errs) > 0) passed := (tst.shouldPass && len(errs) == 0) || (!tst.shouldPass && len(errs) > 0)
require.True(t, passed, "Rule validation failed, rule=\n"+tst.ruleString) require.True(t, passed, "Rule validation failed, rule=\n"+tst.ruleString)
@ -230,7 +242,7 @@ groups:
annotations: annotations:
summary: "Instance {{ $labels.instance }} up" 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") require.Len(t, errs, 2, "Expected two errors")
var err00 *Error var err00 *Error
require.ErrorAs(t, errs[0], &err00) require.ErrorAs(t, errs[0], &err00)

View File

@ -0,0 +1,7 @@
groups:
- name: yolo
rules:
- alert: hola
expr: 1
annotations:
ins-tance: localhost

View File

@ -0,0 +1,7 @@
groups:
- name: yolo
rules:
- alert: hola
expr: 1
annotations:
ins_tance: localhost

View File

@ -428,7 +428,7 @@ func (p *ProtobufParser) Next() (Entry, error) {
// We are at the beginning of a metric family. Put only the name // We are at the beginning of a metric family. Put only the name
// into entryBytes and validate only name, help, and type for now. // into entryBytes and validate only name, help, and type for now.
name := p.dec.GetName() name := p.dec.GetName()
if !model.IsValidMetricName(model.LabelValue(name)) { if !model.UTF8Validation.IsValidMetricName(name) {
return EntryInvalid, fmt.Errorf("invalid metric name: %s", name) return EntryInvalid, fmt.Errorf("invalid metric name: %s", name)
} }
if help := p.dec.GetHelp(); !utf8.ValidString(help) { if help := p.dec.GetHelp(); !utf8.ValidString(help) {

View File

@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog" "github.com/prometheus/common/promslog"
"github.com/prometheus/common/version" "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. // 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 { if o.Do == nil {
o.Do = do o.Do = do
} }
@ -104,6 +105,14 @@ func NewManager(o *Options, logger *slog.Logger) *Manager {
logger = promslog.NewNopLogger() logger = promslog.NewNopLogger()
} }
for _, rc := range o.RelabelConfigs {
switch rc.NameValidationScheme {
case model.LegacyValidation, model.UTF8Validation:
default:
rc.NameValidationScheme = nameValidationScheme
}
}
n := &Manager{ n := &Manager{
queue: make([]*Alert, 0, o.QueueCapacity), queue: make([]*Alert, 0, o.QueueCapacity),
more: make(chan struct{}, 1), 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.ExternalLabels = conf.GlobalConfig.ExternalLabels
n.opts.RelabelConfigs = conf.AlertingConfig.AlertRelabelConfigs 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) amSets := make(map[string]*alertmanagerSet)
// configToAlertmanagers maps alertmanager sets for each unique AlertmanagerConfig, // configToAlertmanagers maps alertmanager sets for each unique AlertmanagerConfig,

View File

@ -45,7 +45,7 @@ import (
const maxBatchSize = 256 const maxBatchSize = 256
func TestHandlerNextBatch(t *testing.T) { func TestHandlerNextBatch(t *testing.T) {
h := NewManager(&Options{}, nil) h := NewManager(&Options{}, model.UTF8Validation, nil)
for i := range make([]struct{}, 2*maxBatchSize+1) { for i := range make([]struct{}, 2*maxBatchSize+1) {
h.queue = append(h.queue, &Alert{ h.queue = append(h.queue, &Alert{
@ -125,7 +125,7 @@ func TestHandlerSendAll(t *testing.T) {
defer server2.Close() defer server2.Close()
defer server3.Close() defer server3.Close()
h := NewManager(&Options{}, nil) h := NewManager(&Options{}, model.UTF8Validation, nil)
authClient, _ := config_util.NewClientFromConfig( authClient, _ := config_util.NewClientFromConfig(
config_util.HTTPClientConfig{ config_util.HTTPClientConfig{
@ -235,7 +235,7 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
defer server2.Close() defer server2.Close()
defer server3.Close() defer server3.Close()
h := NewManager(&Options{}, nil) h := NewManager(&Options{}, model.UTF8Validation, nil)
h.alertmanagers = make(map[string]*alertmanagerSet) h.alertmanagers = make(map[string]*alertmanagerSet)
am1Cfg := config.DefaultAlertmanagerConfig am1Cfg := config.DefaultAlertmanagerConfig
@ -245,9 +245,10 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
am2Cfg.Timeout = model.Duration(time.Second) am2Cfg.Timeout = model.Duration(time.Second)
am2Cfg.AlertRelabelConfigs = []*relabel.Config{ am2Cfg.AlertRelabelConfigs = []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"alertnamedrop"}, SourceLabels: model.LabelNames{"alertnamedrop"},
Action: "drop", Action: "drop",
Regex: relabel.MustNewRegexp(".+"), Regex: relabel.MustNewRegexp(".+"),
NameValidationScheme: model.UTF8Validation,
}, },
} }
@ -255,9 +256,10 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
am3Cfg.Timeout = model.Duration(time.Second) am3Cfg.Timeout = model.Duration(time.Second)
am3Cfg.AlertRelabelConfigs = []*relabel.Config{ am3Cfg.AlertRelabelConfigs = []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"alertname"}, SourceLabels: model.LabelNames{"alertname"},
Action: "drop", Action: "drop",
Regex: relabel.MustNewRegexp(".+"), Regex: relabel.MustNewRegexp(".+"),
NameValidationScheme: model.UTF8Validation,
}, },
} }
@ -374,7 +376,7 @@ func TestCustomDo(t *testing.T) {
Body: io.NopCloser(bytes.NewBuffer(nil)), Body: io.NopCloser(bytes.NewBuffer(nil)),
}, nil }, nil
}, },
}, nil) }, model.UTF8Validation, nil)
h.sendOne(context.Background(), nil, testURL, []byte(testBody)) h.sendOne(context.Background(), nil, testURL, []byte(testBody))
@ -388,14 +390,15 @@ func TestExternalLabels(t *testing.T) {
ExternalLabels: labels.FromStrings("a", "b"), ExternalLabels: labels.FromStrings("a", "b"),
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"alertname"}, SourceLabels: model.LabelNames{"alertname"},
TargetLabel: "a", TargetLabel: "a",
Action: "replace", Action: "replace",
Regex: relabel.MustNewRegexp("externalrelabelthis"), Regex: relabel.MustNewRegexp("externalrelabelthis"),
Replacement: "c", Replacement: "c",
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, nil) }, model.UTF8Validation, nil)
// This alert should get the external label attached. // This alert should get the external label attached.
h.Send(&Alert{ h.Send(&Alert{
@ -422,19 +425,21 @@ func TestHandlerRelabel(t *testing.T) {
MaxBatchSize: maxBatchSize, MaxBatchSize: maxBatchSize,
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"alertname"}, SourceLabels: model.LabelNames{"alertname"},
Action: "drop", Action: "drop",
Regex: relabel.MustNewRegexp("drop"), Regex: relabel.MustNewRegexp("drop"),
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"alertname"}, SourceLabels: model.LabelNames{"alertname"},
TargetLabel: "alertname", TargetLabel: "alertname",
Action: "replace", Action: "replace",
Regex: relabel.MustNewRegexp("rename"), Regex: relabel.MustNewRegexp("rename"),
Replacement: "renamed", Replacement: "renamed",
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, nil) }, model.UTF8Validation, nil)
// This alert should be dropped due to the configuration // This alert should be dropped due to the configuration
h.Send(&Alert{ h.Send(&Alert{
@ -500,6 +505,7 @@ func TestHandlerQueuing(t *testing.T) {
QueueCapacity: 3 * maxBatchSize, QueueCapacity: 3 * maxBatchSize,
MaxBatchSize: maxBatchSize, MaxBatchSize: maxBatchSize,
}, },
model.UTF8Validation,
nil, nil,
) )
@ -606,7 +612,7 @@ func TestReload(t *testing.T) {
}, },
} }
n := NewManager(&Options{}, nil) n := NewManager(&Options{}, model.UTF8Validation, nil)
cfg := &config.Config{} cfg := &config.Config{}
s := ` s := `
@ -653,7 +659,7 @@ func TestDroppedAlertmanagers(t *testing.T) {
}, },
} }
n := NewManager(&Options{}, nil) n := NewManager(&Options{}, model.UTF8Validation, nil)
cfg := &config.Config{} cfg := &config.Config{}
s := ` s := `
@ -766,6 +772,7 @@ func TestHangingNotifier(t *testing.T) {
&Options{ &Options{
QueueCapacity: alertsCount, QueueCapacity: alertsCount,
}, },
model.UTF8Validation,
nil, nil,
) )
notifier.alertmanagers = make(map[string]*alertmanagerSet) notifier.alertmanagers = make(map[string]*alertmanagerSet)
@ -883,6 +890,7 @@ func TestStop_DrainingDisabled(t *testing.T) {
QueueCapacity: 10, QueueCapacity: 10,
DrainOnShutdown: false, DrainOnShutdown: false,
}, },
model.UTF8Validation,
nil, nil,
) )
@ -969,6 +977,7 @@ func TestStop_DrainingEnabled(t *testing.T) {
QueueCapacity: 10, QueueCapacity: 10,
DrainOnShutdown: true, DrainOnShutdown: true,
}, },
model.UTF8Validation,
nil, nil,
) )
@ -1031,7 +1040,7 @@ func TestApplyConfig(t *testing.T) {
} }
alertmanagerURL := fmt.Sprintf("http://%s/api/v2/alerts", targetURL) alertmanagerURL := fmt.Sprintf("http://%s/api/v2/alerts", targetURL)
n := NewManager(&Options{}, nil) n := NewManager(&Options{}, model.UTF8Validation, nil)
cfg := &config.Config{} cfg := &config.Config{}
s := ` s := `
alerting: alerting:

View File

@ -177,6 +177,7 @@ func (m *MetricStreamingDecoder) Label(b scratchBuilder) error {
// via UnsafeAddBytes method to reuse strings. // via UnsafeAddBytes method to reuse strings.
func parseLabel(dAtA []byte, b scratchBuilder) error { func parseLabel(dAtA []byte, b scratchBuilder) error {
var name, value []byte var name, value []byte
var unsafeName string
l := len(dAtA) l := len(dAtA)
iNdEx := 0 iNdEx := 0
for iNdEx < l { for iNdEx < l {
@ -236,8 +237,9 @@ func parseLabel(dAtA []byte, b scratchBuilder) error {
return io.ErrUnexpectedEOF return io.ErrUnexpectedEOF
} }
name = dAtA[iNdEx:postIndex] name = dAtA[iNdEx:postIndex]
if !model.LabelName(name).IsValid() { unsafeName = yoloString(name)
return fmt.Errorf("invalid label name: %s", name) if !model.UTF8Validation.IsValidLabelName(unsafeName) {
return fmt.Errorf("invalid label name: %s", unsafeName)
} }
iNdEx = postIndex iNdEx = postIndex
case 2: case 2:

View File

@ -1676,7 +1676,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value,
if e.Op == parser.COUNT_VALUES { if e.Op == parser.COUNT_VALUES {
valueLabel := param.(*parser.StringLiteral) valueLabel := param.(*parser.StringLiteral)
if !model.LabelName(valueLabel.Val).IsValid() { if !model.UTF8Validation.IsValidLabelName(valueLabel.Val) {
ev.errorf("invalid label name %s", valueLabel) ev.errorf("invalid label name %s", valueLabel)
} }
if !e.Without { if !e.Without {

View File

@ -1584,7 +1584,7 @@ func (ev *evaluator) evalLabelReplace(ctx context.Context, args parser.Expressio
if err != nil { if err != nil {
panic(fmt.Errorf("invalid regular expression in label_replace(): %s", regexStr)) 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)) 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++ { for i := 3; i < len(args); i++ {
src := stringFromArg(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)) panic(fmt.Errorf("invalid source label name in label_join(): %s", src))
} }
srcLabels[i-3] = 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)) panic(fmt.Errorf("invalid destination label name in label_join(): %s", dst))
} }

View File

@ -364,14 +364,14 @@ grouping_label_list:
grouping_label : maybe_label 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) yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", $1.Val)
} }
$$ = $1 $$ = $1
} }
| STRING { | STRING {
unquoted := yylex.(*parser).unquoteString($1.Val) 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) yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", unquoted)
} }
$$ = $1 $$ = $1

View File

@ -1317,7 +1317,7 @@ yydefault:
case 59: case 59:
yyDollar = yyS[yypt-1 : yypt+1] 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) yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", yyDollar[1].item.Val)
} }
yyVAL.item = yyDollar[1].item yyVAL.item = yyDollar[1].item
@ -1326,7 +1326,7 @@ yydefault:
yyDollar = yyS[yypt-1 : yypt+1] yyDollar = yyS[yypt-1 : yypt+1]
{ {
unquoted := yylex.(*parser).unquoteString(yyDollar[1].item.Val) 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) yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", unquoted)
} }
yyVAL.item = yyDollar[1].item yyVAL.item = yyDollar[1].item

View File

@ -100,7 +100,7 @@ func joinLabels(ss []string) string {
if i > 0 { if i > 0 {
b.WriteString(", ") b.WriteString(", ")
} }
if !model.IsValidLegacyMetricName(string(model.LabelValue(s))) { if !model.LegacyValidation.IsValidMetricName(s) {
b.Write(strconv.AppendQuote(b.AvailableBuffer(), s)) b.Write(strconv.AppendQuote(b.AvailableBuffer(), s))
} else { } else {
b.WriteString(s) b.WriteString(s)

View File

@ -771,15 +771,16 @@ func TestSendAlertsDontAffectActiveAlerts(t *testing.T) {
QueueCapacity: 1, QueueCapacity: 1,
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"a1"}, SourceLabels: model.LabelNames{"a1"},
Regex: relabel.MustNewRegexp("(.+)"), Regex: relabel.MustNewRegexp("(.+)"),
TargetLabel: "a1", TargetLabel: "a1",
Replacement: "bug", Replacement: "bug",
Action: "replace", Action: "replace",
NameValidationScheme: model.UTF8Validation,
}, },
}, },
} }
nm := notifier.NewManager(&opts, promslog.NewNopLogger()) nm := notifier.NewManager(&opts, model.UTF8Validation, promslog.NewNopLogger())
f := SendAlerts(nm, "") f := SendAlerts(nm, "")
notifyFunc := func(ctx context.Context, expr string, alerts ...*Alert) { notifyFunc := func(ctx context.Context, expr string, alerts ...*Alert) {

View File

@ -98,7 +98,9 @@ type GroupOptions struct {
func NewGroup(o GroupOptions) *Group { func NewGroup(o GroupOptions) *Group {
opts := o.Opts opts := o.Opts
if opts == nil { if opts == nil {
opts = &ManagerOptions{} opts = &ManagerOptions{
NameValidationScheme: model.UTF8Validation,
}
} }
metrics := opts.Metrics metrics := opts.Metrics
if metrics == nil { if metrics == nil {

View File

@ -26,6 +26,7 @@ import (
"time" "time"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog" "github.com/prometheus/common/promslog"
"golang.org/x/sync/semaphore" "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. // ManagerOptions bundles options for the Manager.
type ManagerOptions struct { type ManagerOptions struct {
NameValidationScheme model.ValidationScheme
ExternalURL *url.URL ExternalURL *url.URL
QueryFunc QueryFunc QueryFunc QueryFunc
NotifyFunc NotifyFunc 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. // GroupLoader is responsible for loading rule groups from arbitrary sources and parsing them.
type GroupLoader interface { 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) Parse(query string) (parser.Expr, error)
} }
@ -297,8 +299,8 @@ type GroupLoader interface {
// and parser.ParseExpr. // and parser.ParseExpr.
type FileLoader struct{} type FileLoader struct{}
func (FileLoader) Load(identifier string, ignoreUnknownFields bool) (*rulefmt.RuleGroups, []error) { func (FileLoader) Load(identifier string, ignoreUnknownFields bool, nameValidationScheme model.ValidationScheme) (*rulefmt.RuleGroups, []error) {
return rulefmt.ParseFile(identifier, ignoreUnknownFields) return rulefmt.ParseFile(identifier, ignoreUnknownFields, nameValidationScheme)
} }
func (FileLoader) Parse(query string) (parser.Expr, error) { return parser.ParseExpr(query) } 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 shouldRestore := !m.restored || m.restoreNewRuleGroups
for _, fn := range filenames { 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 { if errs != nil {
return nil, errs return nil, errs
} }
@ -582,7 +584,7 @@ func FromMaps(maps ...map[string]string) labels.Labels {
} }
// ParseFiles parses the rule files corresponding to glob patterns. // 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{} files := map[string]string{}
for _, pat := range patterns { for _, pat := range patterns {
fns, err := filepath.Glob(pat) fns, err := filepath.Glob(pat)
@ -602,7 +604,7 @@ func ParseFiles(patterns []string) error {
} }
} }
for fn, pat := range files { for fn, pat := range files {
_, errs := rulefmt.ParseFile(fn, false) _, errs := rulefmt.ParseFile(fn, false, nameValidationScheme)
if len(errs) > 0 { if len(errs) > 0 {
return fmt.Errorf("parse rules from file %q (pattern: %q): %w", fn, pat, errors.Join(errs...)) return fmt.Errorf("parse rules from file %q (pattern: %q): %w", fn, pat, errors.Join(errs...))
} }

View File

@ -372,14 +372,15 @@ func TestForStateRestore(t *testing.T) {
ng := testEngine(t) ng := testEngine(t)
opts := &ManagerOptions{ opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(ng, storage), QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {}, NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {},
OutageTolerance: 30 * time.Minute, OutageTolerance: 30 * time.Minute,
ForGracePeriod: 10 * time.Minute, ForGracePeriod: 10 * time.Minute,
NameValidationScheme: model.UTF8Validation,
} }
alertForDuration := 25 * time.Minute alertForDuration := 25 * time.Minute
@ -545,11 +546,12 @@ func TestStaleness(t *testing.T) {
} }
engine := promqltest.NewTestEngineWithOpts(t, engineOpts) engine := promqltest.NewTestEngineWithOpts(t, engineOpts)
opts := &ManagerOptions{ opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(engine, st), QueryFunc: EngineQueryFunc(engine, st),
Appendable: st, Appendable: st,
Queryable: st, Queryable: st,
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("a + 1") expr, err := parser.ParseExpr("a + 1")
@ -647,6 +649,7 @@ groups:
DefaultRuleQueryOffset: func() time.Duration { DefaultRuleQueryOffset: func() time.Duration {
return time.Minute return time.Minute
}, },
NameValidationScheme: model.UTF8Validation,
}) })
m.start() m.start()
err = m.Update(time.Second, []string{fname}, labels.EmptyLabels(), "", nil) err = m.Update(time.Second, []string{fname}, labels.EmptyLabels(), "", nil)
@ -739,6 +742,7 @@ func TestDeletedRuleMarkedStale(t *testing.T) {
opts: &ManagerOptions{ opts: &ManagerOptions{
Appendable: st, Appendable: st,
RuleConcurrencyController: sequentialRuleEvalController{}, RuleConcurrencyController: sequentialRuleEvalController{},
NameValidationScheme: model.UTF8Validation,
}, },
metrics: NewGroupMetrics(nil), metrics: NewGroupMetrics(nil),
} }
@ -779,11 +783,12 @@ func TestUpdate(t *testing.T) {
} }
engine := promqltest.NewTestEngineWithOpts(t, opts) engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Appendable: st, Appendable: st,
Queryable: st, Queryable: st,
QueryFunc: EngineQueryFunc(engine, st), QueryFunc: EngineQueryFunc(engine, st),
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}) })
ruleManager.start() ruleManager.start()
defer ruleManager.Stop() defer ruleManager.Stop()
@ -810,7 +815,7 @@ func TestUpdate(t *testing.T) {
} }
// Groups will be recreated if updated. // 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") require.Empty(t, errs, "file parsing failures")
tmpFile, err := os.CreateTemp("", "rules.test.*.yaml") tmpFile, err := os.CreateTemp("", "rules.test.*.yaml")
@ -923,13 +928,14 @@ func TestNotify(t *testing.T) {
lastNotified = alerts lastNotified = alerts
} }
opts := &ManagerOptions{ opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(engine, storage), QueryFunc: EngineQueryFunc(engine, storage),
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NotifyFunc: notifyFunc, NotifyFunc: notifyFunc,
ResendDelay: 2 * time.Second, ResendDelay: 2 * time.Second,
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("a > 1") expr, err := parser.ParseExpr("a > 1")
@ -994,12 +1000,13 @@ func TestMetricsUpdate(t *testing.T) {
} }
engine := promqltest.NewTestEngineWithOpts(t, opts) engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage), QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
Registerer: registry, Registerer: registry,
NameValidationScheme: model.UTF8Validation,
}) })
ruleManager.start() ruleManager.start()
defer ruleManager.Stop() defer ruleManager.Stop()
@ -1065,11 +1072,12 @@ func TestGroupStalenessOnRemoval(t *testing.T) {
} }
engine := promqltest.NewTestEngineWithOpts(t, opts) engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage), QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}) })
var stopped bool var stopped bool
ruleManager.start() ruleManager.start()
@ -1142,11 +1150,12 @@ func TestMetricsStalenessOnManagerShutdown(t *testing.T) {
} }
engine := promqltest.NewTestEngineWithOpts(t, opts) engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage), QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}) })
var stopped bool var stopped bool
ruleManager.start() ruleManager.start()
@ -1209,11 +1218,12 @@ func TestRuleMovedBetweenGroups(t *testing.T) {
} }
engine := promql.NewEngine(opts) engine := promql.NewEngine(opts)
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage), QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}) })
var stopped bool var stopped bool
ruleManager.start() ruleManager.start()
@ -1291,11 +1301,12 @@ func TestRuleHealthUpdates(t *testing.T) {
} }
engine := promqltest.NewTestEngineWithOpts(t, engineOpts) engine := promqltest.NewTestEngineWithOpts(t, engineOpts)
opts := &ManagerOptions{ opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(engine, st), QueryFunc: EngineQueryFunc(engine, st),
Appendable: st, Appendable: st,
Queryable: st, Queryable: st,
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("a + 1") expr, err := parser.ParseExpr("a + 1")
@ -1389,14 +1400,15 @@ func TestRuleGroupEvalIterationFunc(t *testing.T) {
ng := testEngine(t) ng := testEngine(t)
testFunc := func(tst testInput) { testFunc := func(tst testInput) {
opts := &ManagerOptions{ opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(ng, storage), QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {}, NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {},
OutageTolerance: 30 * time.Minute, OutageTolerance: 30 * time.Minute,
ForGracePeriod: 10 * time.Minute, ForGracePeriod: 10 * time.Minute,
NameValidationScheme: model.UTF8Validation,
} }
activeAlert := &Alert{ activeAlert := &Alert{
@ -1473,11 +1485,12 @@ func TestNativeHistogramsInRecordingRules(t *testing.T) {
ng := testEngine(t) ng := testEngine(t)
opts := &ManagerOptions{ opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(ng, storage), QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage, Appendable: storage,
Queryable: storage, Queryable: storage,
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("sum(histogram_metric)") expr, err := parser.ParseExpr("sum(histogram_metric)")
@ -1524,10 +1537,11 @@ func TestManager_LoadGroups_ShouldCheckWhetherEachRuleHasDependentsAndDependenci
}) })
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
Appendable: storage, Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, 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) { 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) { func TestDependencyMap(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := &ManagerOptions{ opts := &ManagerOptions{
Context: ctx, Context: ctx,
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1638,8 +1653,9 @@ func TestDependencyMap(t *testing.T) {
func TestNoDependency(t *testing.T) { func TestNoDependency(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := &ManagerOptions{ opts := &ManagerOptions{
Context: ctx, Context: ctx,
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1661,8 +1677,9 @@ func TestNoDependency(t *testing.T) {
func TestDependenciesEdgeCases(t *testing.T) { func TestDependenciesEdgeCases(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := &ManagerOptions{ opts := &ManagerOptions{
Context: ctx, Context: ctx,
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
t.Run("empty group", func(t *testing.T) { t.Run("empty group", func(t *testing.T) {
@ -1819,8 +1836,9 @@ func TestDependenciesEdgeCases(t *testing.T) {
func TestNoMetricSelector(t *testing.T) { func TestNoMetricSelector(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := &ManagerOptions{ opts := &ManagerOptions{
Context: ctx, Context: ctx,
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1848,8 +1866,9 @@ func TestNoMetricSelector(t *testing.T) {
func TestDependentRulesWithNonMetricExpression(t *testing.T) { func TestDependentRulesWithNonMetricExpression(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := &ManagerOptions{ opts := &ManagerOptions{
Context: ctx, Context: ctx,
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
} }
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))") expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1880,8 +1899,9 @@ func TestDependentRulesWithNonMetricExpression(t *testing.T) {
func TestRulesDependentOnMetaMetrics(t *testing.T) { func TestRulesDependentOnMetaMetrics(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := &ManagerOptions{ opts := &ManagerOptions{
Context: ctx, Context: ctx,
Logger: promslog.NewNopLogger(), 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 // 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) { func TestDependencyMapUpdatesOnGroupUpdate(t *testing.T) {
files := []string{"fixtures/rules.yaml"} files := []string{"fixtures/rules.yaml"}
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}) })
ruleManager.start() ruleManager.start()
@ -2447,8 +2468,9 @@ func TestBoundedRuleEvalConcurrency(t *testing.T) {
func TestUpdateWhenStopped(t *testing.T) { func TestUpdateWhenStopped(t *testing.T) {
files := []string{"fixtures/rules.yaml"} files := []string{"fixtures/rules.yaml"}
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}) })
ruleManager.start() ruleManager.start()
err := ruleManager.Update(10*time.Second, files, labels.EmptyLabels(), "", nil) 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, ConcurrentEvalsEnabled: concurrent,
MaxConcurrentEvals: maxConcurrent, MaxConcurrentEvals: maxConcurrent,
Appendable: storage, Appendable: storage,
NameValidationScheme: model.UTF8Validation,
QueryFunc: func(_ context.Context, _ string, ts time.Time) (promql.Vector, error) { QueryFunc: func(_ context.Context, _ string, ts time.Time) (promql.Vector, error) {
inflightMu.Lock() inflightMu.Lock()
@ -2552,11 +2575,11 @@ func TestLabels_FromMaps(t *testing.T) {
func TestParseFiles(t *testing.T) { func TestParseFiles(t *testing.T) {
t.Run("good files", func(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) require.NoError(t, err)
}) })
t.Run("bad files", func(t *testing.T) { 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") 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() }) t.Cleanup(func() { storage.Close() })
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Context: context.Background(), Context: context.Background(),
Logger: promslog.NewNopLogger(), Logger: promslog.NewNopLogger(),
Appendable: storage, Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, 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) 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() }) b.Cleanup(func() { storage.Close() })
ruleManager := NewManager(&ManagerOptions{ ruleManager := NewManager(&ManagerOptions{
Context: context.Background(), NameValidationScheme: model.UTF8Validation,
Logger: promslog.NewNopLogger(), Context: context.Background(),
Appendable: storage, Logger: promslog.NewNopLogger(),
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil }, 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") groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, "fixtures/rules_multiple.yaml")

View File

@ -188,11 +188,12 @@ func TestPopulateLabels(t *testing.T) {
ScrapeTimeout: model.Duration(time.Second), ScrapeTimeout: model.Duration(time.Second),
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
Action: relabel.Replace, Action: relabel.Replace,
Regex: relabel.MustNewRegexp("(.*)"), Regex: relabel.MustNewRegexp("(.*)"),
SourceLabels: model.LabelNames{"custom"}, SourceLabels: model.LabelNames{"custom"},
Replacement: "${1}", Replacement: "${1}",
TargetLabel: string(model.AddressLabel), TargetLabel: string(model.AddressLabel),
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, },
@ -226,11 +227,12 @@ func TestPopulateLabels(t *testing.T) {
ScrapeTimeout: model.Duration(time.Second), ScrapeTimeout: model.Duration(time.Second),
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
Action: relabel.Replace, Action: relabel.Replace,
Regex: relabel.MustNewRegexp("(.*)"), Regex: relabel.MustNewRegexp("(.*)"),
SourceLabels: model.LabelNames{"custom"}, SourceLabels: model.LabelNames{"custom"},
Replacement: "${1}", Replacement: "${1}",
TargetLabel: string(model.AddressLabel), TargetLabel: string(model.AddressLabel),
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, },
@ -450,6 +452,10 @@ func TestPopulateLabels(t *testing.T) {
for _, c := range cases { for _, c := range cases {
in := maps.Clone(c.in) in := maps.Clone(c.in)
lb := labels.NewBuilder(labels.EmptyLabels()) 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) res, err := PopulateLabels(lb, c.cfg, c.in, nil)
if c.err != "" { if c.err != "" {
require.EqualError(t, err, c.err) require.EqualError(t, err, c.err)

View File

@ -154,6 +154,11 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed
return nil, err 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 var escapingScheme model.EscapingScheme
escapingScheme, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme) escapingScheme, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme)
if err != nil { if err != nil {
@ -326,6 +331,11 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error {
sp.config = cfg sp.config = cfg
oldClient := sp.client oldClient := sp.client
sp.client = 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 sp.validationScheme = cfg.MetricNameValidationScheme
var escapingScheme model.EscapingScheme var escapingScheme model.EscapingScheme
escapingScheme, err = model.ToEscapingScheme(cfg.MetricNameEscapingScheme) escapingScheme, err = model.ToEscapingScheme(cfg.MetricNameEscapingScheme)

View File

@ -334,9 +334,10 @@ func TestDroppedTargetsList(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8, MetricNameEscapingScheme: model.AllowUTF8,
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
Action: relabel.Drop, Action: relabel.Drop,
Regex: relabel.MustNewRegexp("dropMe"), Regex: relabel.MustNewRegexp("dropMe"),
SourceLabels: model.LabelNames{"job"}, 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"), labels: labels.FromStrings("pod_label_invalid_012\xff", "test"),
} }
relabelConfig := []*relabel.Config{{ relabelConfig := []*relabel.Config{{
Action: relabel.LabelMap, Action: relabel.LabelMap,
Regex: relabel.MustNewRegexp("pod_label_invalid_(.+)"), Regex: relabel.MustNewRegexp("pod_label_invalid_(.+)"),
Separator: ";", Separator: ";",
Replacement: "$1", Replacement: "$1",
NameValidationScheme: model.UTF8Validation,
}} }}
sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0) sl := newBasicScrapeLoop(t, ctx, &testScraper{}, s.Appender, 0)
sl.sampleMutator = func(l labels.Labels) labels.Labels { sl.sampleMutator = func(l labels.Labels) labels.Labels {
@ -4182,18 +4184,20 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
MetricNameEscapingScheme: model.AllowUTF8, MetricNameEscapingScheme: model.AllowUTF8,
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{model.ScrapeIntervalLabel}, SourceLabels: model.LabelNames{model.ScrapeIntervalLabel},
Regex: relabel.MustNewRegexp("2s"), Regex: relabel.MustNewRegexp("2s"),
Replacement: "3s", Replacement: "3s",
TargetLabel: model.ScrapeIntervalLabel, TargetLabel: model.ScrapeIntervalLabel,
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{model.ScrapeTimeoutLabel}, SourceLabels: model.LabelNames{model.ScrapeTimeoutLabel},
Regex: relabel.MustNewRegexp("500ms"), Regex: relabel.MustNewRegexp("500ms"),
Replacement: "750ms", Replacement: "750ms",
TargetLabel: model.ScrapeTimeoutLabel, TargetLabel: model.ScrapeTimeoutLabel,
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
} }
@ -4220,20 +4224,22 @@ func TestLeQuantileReLabel(t *testing.T) {
JobName: "test", JobName: "test",
MetricRelabelConfigs: []*relabel.Config{ MetricRelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"le", "__name__"}, SourceLabels: model.LabelNames{"le", "__name__"},
Regex: relabel.MustNewRegexp("(\\d+)\\.0+;.*_bucket"), Regex: relabel.MustNewRegexp("(\\d+)\\.0+;.*_bucket"),
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Separator: relabel.DefaultRelabelConfig.Separator, Separator: relabel.DefaultRelabelConfig.Separator,
TargetLabel: "le", TargetLabel: "le",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"quantile"}, SourceLabels: model.LabelNames{"quantile"},
Regex: relabel.MustNewRegexp("(\\d+)\\.0+"), Regex: relabel.MustNewRegexp("(\\d+)\\.0+"),
Replacement: relabel.DefaultRelabelConfig.Replacement, Replacement: relabel.DefaultRelabelConfig.Replacement,
Separator: relabel.DefaultRelabelConfig.Separator, Separator: relabel.DefaultRelabelConfig.Separator,
TargetLabel: "quantile", TargetLabel: "quantile",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
SampleLimit: 100, SampleLimit: 100,
@ -4862,18 +4868,20 @@ func TestTypeUnitReLabel(t *testing.T) {
JobName: "test", JobName: "test",
MetricRelabelConfigs: []*relabel.Config{ MetricRelabelConfigs: []*relabel.Config{
{ {
SourceLabels: model.LabelNames{"__name__"}, SourceLabels: model.LabelNames{"__name__"},
Regex: relabel.MustNewRegexp(".*_total$"), Regex: relabel.MustNewRegexp(".*_total$"),
Replacement: "counter", Replacement: "counter",
TargetLabel: "__type__", TargetLabel: "__type__",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
SourceLabels: model.LabelNames{"__name__"}, SourceLabels: model.LabelNames{"__name__"},
Regex: relabel.MustNewRegexp(".*_bytes$"), Regex: relabel.MustNewRegexp(".*_bytes$"),
Replacement: "bytes", Replacement: "bytes",
TargetLabel: "__unit__", TargetLabel: "__unit__",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
SampleLimit: 100, SampleLimit: 100,
@ -5482,25 +5490,28 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
Params: url.Values{"param": []string{secondParam}}, Params: url.Values{"param": []string{secondParam}},
RelabelConfigs: []*relabel.Config{ RelabelConfigs: []*relabel.Config{
{ {
Action: relabel.DefaultRelabelConfig.Action, Action: relabel.DefaultRelabelConfig.Action,
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, SourceLabels: relabel.DefaultRelabelConfig.SourceLabels,
TargetLabel: model.ScrapeTimeoutLabel, TargetLabel: model.ScrapeTimeoutLabel,
Replacement: expectedTimeoutLabel, Replacement: expectedTimeoutLabel,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
Action: relabel.DefaultRelabelConfig.Action, Action: relabel.DefaultRelabelConfig.Action,
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, SourceLabels: relabel.DefaultRelabelConfig.SourceLabels,
TargetLabel: paramLabel, TargetLabel: paramLabel,
Replacement: expectedParam, Replacement: expectedParam,
NameValidationScheme: model.UTF8Validation,
}, },
{ {
Action: relabel.DefaultRelabelConfig.Action, Action: relabel.DefaultRelabelConfig.Action,
Regex: relabel.DefaultRelabelConfig.Regex, Regex: relabel.DefaultRelabelConfig.Regex,
SourceLabels: relabel.DefaultRelabelConfig.SourceLabels, SourceLabels: relabel.DefaultRelabelConfig.SourceLabels,
TargetLabel: model.MetricsPathLabel, TargetLabel: model.MetricsPathLabel,
Replacement: expectedPath, Replacement: expectedPath,
NameValidationScheme: model.UTF8Validation,
}, },
}, },
}, },

View File

@ -766,10 +766,10 @@ func (it *chunkedSeriesIterator) Err() error {
// also making sure that there are no labels with duplicate names. // also making sure that there are no labels with duplicate names.
func validateLabelsAndMetricName(ls []prompb.Label) error { func validateLabelsAndMetricName(ls []prompb.Label) error {
for i, l := range ls { 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) 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) return fmt.Errorf("invalid label name: %v", l.Name)
} }
if !model.LabelValue(l.Value).IsValid() { if !model.LabelValue(l.Value).IsValid() {

View File

@ -1418,12 +1418,13 @@ func BenchmarkStoreSeries(b *testing.B) {
{Name: "replica", Value: "1"}, {Name: "replica", Value: "1"},
} }
relabelConfigs := []*relabel.Config{{ relabelConfigs := []*relabel.Config{{
SourceLabels: model.LabelNames{"namespace"}, SourceLabels: model.LabelNames{"namespace"},
Separator: ";", Separator: ";",
Regex: relabel.MustNewRegexp("kube.*"), Regex: relabel.MustNewRegexp("kube.*"),
TargetLabel: "job", TargetLabel: "job",
Replacement: "$1", Replacement: "$1",
Action: relabel.Replace, Action: relabel.Replace,
NameValidationScheme: model.UTF8Validation,
}} }}
testCases := []struct { testCases := []struct {
name string name string

View File

@ -282,7 +282,8 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) {
QueueConfig: config.DefaultQueueConfig, QueueConfig: config.DefaultQueueConfig,
WriteRelabelConfigs: []*relabel.Config{ WriteRelabelConfigs: []*relabel.Config{
{ {
Regex: relabel.MustNewRegexp(".+"), Regex: relabel.MustNewRegexp(".+"),
NameValidationScheme: model.UTF8Validation,
}, },
}, },
ProtobufMessage: config.RemoteWriteProtoMsgV1, ProtobufMessage: config.RemoteWriteProtoMsgV1,
@ -329,7 +330,10 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) {
storeHashes() storeHashes()
// Update c0 and c2. // 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) c2.RemoteTimeout = model.Duration(50 * time.Second)
conf = &config.Config{ conf = &config.Config{
GlobalConfig: config.GlobalConfig{}, GlobalConfig: config.GlobalConfig{},

View File

@ -790,8 +790,7 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) {
name = model.UnescapeName(name, model.ValueEncodingEscaping) name = model.UnescapeName(name, model.ValueEncodingEscaping)
} }
label := model.LabelName(name) if !model.UTF8Validation.IsValidLabelName(name) {
if !label.IsValid() {
return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil} return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil}
} }

View File

@ -311,11 +311,12 @@ func (m *rulesRetrieverMock) CreateRuleGroups() {
} }
engine := promqltest.NewTestEngineWithOpts(m.testing, engineOpts) engine := promqltest.NewTestEngineWithOpts(m.testing, engineOpts)
opts := &rules.ManagerOptions{ opts := &rules.ManagerOptions{
QueryFunc: rules.EngineQueryFunc(engine, storage), NameValidationScheme: model.UTF8Validation,
Appendable: storage, QueryFunc: rules.EngineQueryFunc(engine, storage),
Context: context.Background(), Appendable: storage,
Logger: promslog.NewNopLogger(), Context: context.Background(),
NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {}, Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*rules.Alert) {},
} }
var r []rules.Rule var r []rules.Rule