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.
if err := rules.ParseFiles(cfgFile.RuleFiles); err != nil {
if err := rules.ParseFiles(cfgFile.RuleFiles, cfgFile.GlobalConfig.MetricNameValidationScheme); err != nil {
absPath, pathErr := filepath.Abs(cfg.configFile)
if pathErr != nil {
absPath = cfg.configFile
@ -790,7 +790,7 @@ func main() {
ctxWeb, cancelWeb = context.WithCancel(context.Background())
ctxRule = context.Background()
notifierManager = notifier.NewManager(&cfg.notifier, logger.With("component", "notifier"))
notifierManager = notifier.NewManager(&cfg.notifier, cfgFile.GlobalConfig.MetricNameValidationScheme, logger.With("component", "notifier"))
ctxScrape, cancelScrape = context.WithCancel(context.Background())
ctxNotify, cancelNotify = context.WithCancel(context.Background())
@ -867,6 +867,7 @@ func main() {
queryEngine = promql.NewEngine(opts)
ruleManager = rules.NewManager(&rules.ManagerOptions{
NameValidationScheme: cfgFile.GlobalConfig.MetricNameValidationScheme,
Appendable: fanoutStorage,
Queryable: localStorage,
QueryFunc: rules.EngineQueryFunc(queryEngine, fanoutStorage),

View File

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

View File

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

View File

@ -48,11 +48,12 @@ type ruleImporter struct {
}
type ruleImporterConfig struct {
outputDir string
start time.Time
end time.Time
evalInterval time.Duration
maxBlockDuration time.Duration
outputDir string
start time.Time
end time.Time
evalInterval time.Duration
maxBlockDuration time.Duration
nameValidationScheme model.ValidationScheme
}
// newRuleImporter creates a new rule importer that can be used to parse and evaluate recording rule files and create new series
@ -60,10 +61,12 @@ type ruleImporterConfig struct {
func newRuleImporter(logger *slog.Logger, config ruleImporterConfig, apiClient queryRangeAPI) *ruleImporter {
logger.Info("new rule importer", "component", "backfiller", "start", config.start.Format(time.RFC822), "end", config.end.Format(time.RFC822))
return &ruleImporter{
logger: logger,
config: config,
apiClient: apiClient,
ruleManager: rules.NewManager(&rules.ManagerOptions{}),
logger: logger,
config: config,
apiClient: apiClient,
ruleManager: rules.NewManager(&rules.ManagerOptions{
NameValidationScheme: config.nameValidationScheme,
}),
}
}

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) {
logger := promslog.NewNopLogger()
cfg := ruleImporterConfig{
outputDir: tmpDir,
start: start.Add(-10 * time.Hour),
end: start.Add(-7 * time.Hour),
evalInterval: 60 * time.Second,
maxBlockDuration: maxBlockDuration,
outputDir: tmpDir,
start: start.Add(-10 * time.Hour),
end: start.Add(-7 * time.Hour),
evalInterval: 60 * time.Second,
maxBlockDuration: maxBlockDuration,
nameValidationScheme: model.UTF8Validation,
}
return newRuleImporter(logger, cfg, mockQueryRangeAPI{

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

@ -50,7 +50,7 @@ require (
github.com/prometheus/alertmanager v0.28.1
github.com/prometheus/client_golang v1.23.0-rc.1
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3
github.com/prometheus/common v0.65.1-0.20250801071412-c79a891c6c28
github.com/prometheus/common/assets v0.2.0
github.com/prometheus/exporter-toolkit v0.14.0
github.com/prometheus/sigv4 v0.2.0

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/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3 h1:R/zO7ombSHCI8bjQusgCMSL+cE669w5/R2upq5WlPD0=
github.com/prometheus/common v0.65.1-0.20250703115700-7f8b2a0d32d3/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.65.1-0.20250801071412-c79a891c6c28 h1:9CaJtf5ZS3GQVCVoslEkJcKSVwiD9aTqwgMpG1n9zQw=
github.com/prometheus/common v0.65.1-0.20250801071412-c79a891c6c28/go.mod h1:LL3lcZII3UXGO4InbF+BTSsiAAPUBnwFVbp4gBWIMqw=
github.com/prometheus/common/assets v0.2.0 h1:0P5OrzoHrYBOSM1OigWL3mY8ZvV2N4zIE/5AahrSrfM=
github.com/prometheus/common/assets v0.2.0/go.mod h1:D17UVUE12bHbim7HzwUvtqm6gwBEaDQ0F+hIGbFbccI=
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=

View File

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

View File

@ -100,6 +100,8 @@ type Config struct {
Replacement string `yaml:"replacement,omitempty" json:"replacement,omitempty"`
// Action is the action to be performed for the relabeling.
Action Action `yaml:"action,omitempty" json:"action,omitempty"`
// NameValidationScheme to use when validating labels.
NameValidationScheme model.ValidationScheme `yaml:"-" json:"-"`
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
@ -112,10 +114,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if c.Regex.Regexp == nil {
c.Regex = MustNewRegexp("")
}
return c.Validate()
return nil
}
func (c *Config) Validate() error {
func (c *Config) Validate(nameValidationScheme model.ValidationScheme) error {
if c.Action == "" {
return errors.New("relabel action cannot be empty")
}
@ -125,7 +127,17 @@ func (c *Config) Validate() error {
if (c.Action == Replace || c.Action == HashMod || c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && c.TargetLabel == "" {
return fmt.Errorf("relabel configuration for %s action requires 'target_label' value", c.Action)
}
if c.Action == Replace && !varInRegexTemplate(c.TargetLabel) && !model.LabelName(c.TargetLabel).IsValid() {
// Relabel config validation scheme matches global if left blank.
switch c.NameValidationScheme {
case model.LegacyValidation, model.UTF8Validation:
case model.UnsetValidation:
c.NameValidationScheme = nameValidationScheme
default:
return fmt.Errorf("unknown relabel config name validation method specified, must be either '', 'legacy' or 'utf8', got %s", c.NameValidationScheme)
}
if c.Action == Replace && !varInRegexTemplate(c.TargetLabel) && !c.NameValidationScheme.IsValidLabelName(c.TargetLabel) {
return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action)
}
@ -133,12 +145,12 @@ func (c *Config) Validate() error {
// UTF-8 allows ${} characters, so standard validation allow $variables by default.
// TODO(bwplotka): Relabelling users cannot put $ and ${<...>} characters in metric names or values.
// Design escaping mechanism to allow that, once valid use case appears.
return model.LabelName(value).IsValid()
return c.NameValidationScheme.IsValidLabelName(value)
}
if c.Action == Replace && varInRegexTemplate(c.TargetLabel) && !isValidLabelNameWithRegexVarFn(c.TargetLabel) {
return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action)
}
if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && !model.LabelName(c.TargetLabel).IsValid() {
if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && !c.NameValidationScheme.IsValidLabelName(c.TargetLabel) {
return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action)
}
if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && c.Replacement != DefaultRelabelConfig.Replacement {
@ -147,7 +159,7 @@ func (c *Config) Validate() error {
if c.Action == LabelMap && !isValidLabelNameWithRegexVarFn(c.Replacement) {
return fmt.Errorf("%q is invalid 'replacement' for %s action", c.Replacement, c.Action)
}
if c.Action == HashMod && !model.LabelName(c.TargetLabel).IsValid() {
if c.Action == HashMod && !c.NameValidationScheme.IsValidLabelName(c.TargetLabel) {
return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action)
}
@ -318,16 +330,16 @@ func relabel(cfg *Config, lb *labels.Builder) (keep bool) {
if indexes == nil {
break
}
target := model.LabelName(cfg.Regex.ExpandString([]byte{}, cfg.TargetLabel, val, indexes))
if !target.IsValid() {
target := string(cfg.Regex.ExpandString([]byte{}, cfg.TargetLabel, val, indexes))
if !cfg.NameValidationScheme.IsValidLabelName(target) {
break
}
res := cfg.Regex.ExpandString([]byte{}, cfg.Replacement, val, indexes)
if len(res) == 0 {
lb.Del(string(target))
lb.Del(target)
break
}
lb.Set(string(target), string(res))
lb.Set(target, string(res))
case Lowercase:
lb.Set(cfg.TargetLabel, strings.ToLower(val))
case Uppercase:

View File

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

View File

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

View File

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

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
// into entryBytes and validate only name, help, and type for now.
name := p.dec.GetName()
if !model.IsValidMetricName(model.LabelValue(name)) {
if !model.UTF8Validation.IsValidMetricName(name) {
return EntryInvalid, fmt.Errorf("invalid metric name: %s", name)
}
if help := p.dec.GetHelp(); !utf8.ValidString(help) {

View File

@ -26,6 +26,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/prometheus/common/version"
@ -92,7 +93,7 @@ func do(ctx context.Context, client *http.Client, req *http.Request) (*http.Resp
}
// NewManager is the manager constructor.
func NewManager(o *Options, logger *slog.Logger) *Manager {
func NewManager(o *Options, nameValidationScheme model.ValidationScheme, logger *slog.Logger) *Manager {
if o.Do == nil {
o.Do = do
}
@ -104,6 +105,14 @@ func NewManager(o *Options, logger *slog.Logger) *Manager {
logger = promslog.NewNopLogger()
}
for _, rc := range o.RelabelConfigs {
switch rc.NameValidationScheme {
case model.LegacyValidation, model.UTF8Validation:
default:
rc.NameValidationScheme = nameValidationScheme
}
}
n := &Manager{
queue: make([]*Alert, 0, o.QueueCapacity),
more: make(chan struct{}, 1),
@ -133,6 +142,13 @@ func (n *Manager) ApplyConfig(conf *config.Config) error {
n.opts.ExternalLabels = conf.GlobalConfig.ExternalLabels
n.opts.RelabelConfigs = conf.AlertingConfig.AlertRelabelConfigs
for i, rc := range n.opts.RelabelConfigs {
switch rc.NameValidationScheme {
case model.LegacyValidation, model.UTF8Validation:
default:
n.opts.RelabelConfigs[i].NameValidationScheme = conf.GlobalConfig.MetricNameValidationScheme
}
}
amSets := make(map[string]*alertmanagerSet)
// configToAlertmanagers maps alertmanager sets for each unique AlertmanagerConfig,

View File

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

View File

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

View File

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

View File

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

View File

@ -364,14 +364,14 @@ grouping_label_list:
grouping_label : maybe_label
{
if !model.LabelName($1.Val).IsValid() {
if !model.UTF8Validation.IsValidLabelName($1.Val) {
yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", $1.Val)
}
$$ = $1
}
| STRING {
unquoted := yylex.(*parser).unquoteString($1.Val)
if !model.LabelName(unquoted).IsValid() {
if !model.UTF8Validation.IsValidLabelName(unquoted) {
yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", unquoted)
}
$$ = $1

View File

@ -1317,7 +1317,7 @@ yydefault:
case 59:
yyDollar = yyS[yypt-1 : yypt+1]
{
if !model.LabelName(yyDollar[1].item.Val).IsValid() {
if !model.UTF8Validation.IsValidLabelName(yyDollar[1].item.Val) {
yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", yyDollar[1].item.Val)
}
yyVAL.item = yyDollar[1].item
@ -1326,7 +1326,7 @@ yydefault:
yyDollar = yyS[yypt-1 : yypt+1]
{
unquoted := yylex.(*parser).unquoteString(yyDollar[1].item.Val)
if !model.LabelName(unquoted).IsValid() {
if !model.UTF8Validation.IsValidLabelName(unquoted) {
yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", unquoted)
}
yyVAL.item = yyDollar[1].item

View File

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

View File

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

View File

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

View File

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

View File

@ -372,14 +372,15 @@ func TestForStateRestore(t *testing.T) {
ng := testEngine(t)
opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {},
OutageTolerance: 30 * time.Minute,
ForGracePeriod: 10 * time.Minute,
QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {},
OutageTolerance: 30 * time.Minute,
ForGracePeriod: 10 * time.Minute,
NameValidationScheme: model.UTF8Validation,
}
alertForDuration := 25 * time.Minute
@ -545,11 +546,12 @@ func TestStaleness(t *testing.T) {
}
engine := promqltest.NewTestEngineWithOpts(t, engineOpts)
opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(engine, st),
Appendable: st,
Queryable: st,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
QueryFunc: EngineQueryFunc(engine, st),
Appendable: st,
Queryable: st,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("a + 1")
@ -647,6 +649,7 @@ groups:
DefaultRuleQueryOffset: func() time.Duration {
return time.Minute
},
NameValidationScheme: model.UTF8Validation,
})
m.start()
err = m.Update(time.Second, []string{fname}, labels.EmptyLabels(), "", nil)
@ -739,6 +742,7 @@ func TestDeletedRuleMarkedStale(t *testing.T) {
opts: &ManagerOptions{
Appendable: st,
RuleConcurrencyController: sequentialRuleEvalController{},
NameValidationScheme: model.UTF8Validation,
},
metrics: NewGroupMetrics(nil),
}
@ -779,11 +783,12 @@ func TestUpdate(t *testing.T) {
}
engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{
Appendable: st,
Queryable: st,
QueryFunc: EngineQueryFunc(engine, st),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: st,
Queryable: st,
QueryFunc: EngineQueryFunc(engine, st),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
})
ruleManager.start()
defer ruleManager.Stop()
@ -810,7 +815,7 @@ func TestUpdate(t *testing.T) {
}
// Groups will be recreated if updated.
rgs, errs := rulefmt.ParseFile("fixtures/rules.yaml", false)
rgs, errs := rulefmt.ParseFile("fixtures/rules.yaml", false, model.UTF8Validation)
require.Empty(t, errs, "file parsing failures")
tmpFile, err := os.CreateTemp("", "rules.test.*.yaml")
@ -923,13 +928,14 @@ func TestNotify(t *testing.T) {
lastNotified = alerts
}
opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(engine, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: notifyFunc,
ResendDelay: 2 * time.Second,
QueryFunc: EngineQueryFunc(engine, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: notifyFunc,
ResendDelay: 2 * time.Second,
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("a > 1")
@ -994,12 +1000,13 @@ func TestMetricsUpdate(t *testing.T) {
}
engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Registerer: registry,
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Registerer: registry,
NameValidationScheme: model.UTF8Validation,
})
ruleManager.start()
defer ruleManager.Stop()
@ -1065,11 +1072,12 @@ func TestGroupStalenessOnRemoval(t *testing.T) {
}
engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
})
var stopped bool
ruleManager.start()
@ -1142,11 +1150,12 @@ func TestMetricsStalenessOnManagerShutdown(t *testing.T) {
}
engine := promqltest.NewTestEngineWithOpts(t, opts)
ruleManager := NewManager(&ManagerOptions{
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
})
var stopped bool
ruleManager.start()
@ -1209,11 +1218,12 @@ func TestRuleMovedBetweenGroups(t *testing.T) {
}
engine := promql.NewEngine(opts)
ruleManager := NewManager(&ManagerOptions{
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
Queryable: storage,
QueryFunc: EngineQueryFunc(engine, storage),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
})
var stopped bool
ruleManager.start()
@ -1291,11 +1301,12 @@ func TestRuleHealthUpdates(t *testing.T) {
}
engine := promqltest.NewTestEngineWithOpts(t, engineOpts)
opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(engine, st),
Appendable: st,
Queryable: st,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
QueryFunc: EngineQueryFunc(engine, st),
Appendable: st,
Queryable: st,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("a + 1")
@ -1389,14 +1400,15 @@ func TestRuleGroupEvalIterationFunc(t *testing.T) {
ng := testEngine(t)
testFunc := func(tst testInput) {
opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {},
OutageTolerance: 30 * time.Minute,
ForGracePeriod: 10 * time.Minute,
QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NotifyFunc: func(_ context.Context, _ string, _ ...*Alert) {},
OutageTolerance: 30 * time.Minute,
ForGracePeriod: 10 * time.Minute,
NameValidationScheme: model.UTF8Validation,
}
activeAlert := &Alert{
@ -1473,11 +1485,12 @@ func TestNativeHistogramsInRecordingRules(t *testing.T) {
ng := testEngine(t)
opts := &ManagerOptions{
QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
QueryFunc: EngineQueryFunc(ng, storage),
Appendable: storage,
Queryable: storage,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("sum(histogram_metric)")
@ -1524,10 +1537,11 @@ func TestManager_LoadGroups_ShouldCheckWhetherEachRuleHasDependentsAndDependenci
})
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil },
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil },
NameValidationScheme: model.UTF8Validation,
})
t.Run("load a mix of dependent and independent rules", func(t *testing.T) {
@ -1580,8 +1594,9 @@ func TestManager_LoadGroups_ShouldCheckWhetherEachRuleHasDependentsAndDependenci
func TestDependencyMap(t *testing.T) {
ctx := context.Background()
opts := &ManagerOptions{
Context: ctx,
Logger: promslog.NewNopLogger(),
Context: ctx,
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1638,8 +1653,9 @@ func TestDependencyMap(t *testing.T) {
func TestNoDependency(t *testing.T) {
ctx := context.Background()
opts := &ManagerOptions{
Context: ctx,
Logger: promslog.NewNopLogger(),
Context: ctx,
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1661,8 +1677,9 @@ func TestNoDependency(t *testing.T) {
func TestDependenciesEdgeCases(t *testing.T) {
ctx := context.Background()
opts := &ManagerOptions{
Context: ctx,
Logger: promslog.NewNopLogger(),
Context: ctx,
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
t.Run("empty group", func(t *testing.T) {
@ -1819,8 +1836,9 @@ func TestDependenciesEdgeCases(t *testing.T) {
func TestNoMetricSelector(t *testing.T) {
ctx := context.Background()
opts := &ManagerOptions{
Context: ctx,
Logger: promslog.NewNopLogger(),
Context: ctx,
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1848,8 +1866,9 @@ func TestNoMetricSelector(t *testing.T) {
func TestDependentRulesWithNonMetricExpression(t *testing.T) {
ctx := context.Background()
opts := &ManagerOptions{
Context: ctx,
Logger: promslog.NewNopLogger(),
Context: ctx,
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
expr, err := parser.ParseExpr("sum by (user) (rate(requests[1m]))")
@ -1880,8 +1899,9 @@ func TestDependentRulesWithNonMetricExpression(t *testing.T) {
func TestRulesDependentOnMetaMetrics(t *testing.T) {
ctx := context.Background()
opts := &ManagerOptions{
Context: ctx,
Logger: promslog.NewNopLogger(),
Context: ctx,
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
}
// This rule is not dependent on any other rules in its group but it does depend on `ALERTS`, which is produced by
@ -1909,8 +1929,9 @@ func TestRulesDependentOnMetaMetrics(t *testing.T) {
func TestDependencyMapUpdatesOnGroupUpdate(t *testing.T) {
files := []string{"fixtures/rules.yaml"}
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
})
ruleManager.start()
@ -2447,8 +2468,9 @@ func TestBoundedRuleEvalConcurrency(t *testing.T) {
func TestUpdateWhenStopped(t *testing.T) {
files := []string{"fixtures/rules.yaml"}
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Context: context.Background(),
Logger: promslog.NewNopLogger(),
NameValidationScheme: model.UTF8Validation,
})
ruleManager.start()
err := ruleManager.Update(10*time.Second, files, labels.EmptyLabels(), "", nil)
@ -2509,6 +2531,7 @@ func optsFactory(storage storage.Storage, maxInflight, inflightQueries *atomic.I
ConcurrentEvalsEnabled: concurrent,
MaxConcurrentEvals: maxConcurrent,
Appendable: storage,
NameValidationScheme: model.UTF8Validation,
QueryFunc: func(_ context.Context, _ string, ts time.Time) (promql.Vector, error) {
inflightMu.Lock()
@ -2552,11 +2575,11 @@ func TestLabels_FromMaps(t *testing.T) {
func TestParseFiles(t *testing.T) {
t.Run("good files", func(t *testing.T) {
err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")})
err := ParseFiles([]string{filepath.Join("fixtures", "rules.y*ml")}, model.UTF8Validation)
require.NoError(t, err)
})
t.Run("bad files", func(t *testing.T) {
err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")})
err := ParseFiles([]string{filepath.Join("fixtures", "invalid_rules.y*ml")}, model.UTF8Validation)
require.ErrorContains(t, err, "field unexpected_field not found in type rulefmt.Rule")
})
}
@ -2666,10 +2689,11 @@ func TestRuleDependencyController_AnalyseRules(t *testing.T) {
t.Cleanup(func() { storage.Close() })
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil },
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
NameValidationScheme: model.UTF8Validation,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil },
})
groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, tc.ruleFile)
@ -2695,10 +2719,11 @@ func BenchmarkRuleDependencyController_AnalyseRules(b *testing.B) {
b.Cleanup(func() { storage.Close() })
ruleManager := NewManager(&ManagerOptions{
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil },
NameValidationScheme: model.UTF8Validation,
Context: context.Background(),
Logger: promslog.NewNopLogger(),
Appendable: storage,
QueryFunc: func(_ context.Context, _ string, _ time.Time) (promql.Vector, error) { return nil, nil },
})
groups, errs := ruleManager.LoadGroups(time.Second, labels.EmptyLabels(), "", nil, false, "fixtures/rules_multiple.yaml")

View File

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

View File

@ -154,6 +154,11 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed
return nil, err
}
switch cfg.MetricNameValidationScheme {
case model.LegacyValidation, model.UTF8Validation:
default:
return nil, errors.New("newScrapePool: MetricNameValidationScheme must be set in scrape configuration")
}
var escapingScheme model.EscapingScheme
escapingScheme, err = config.ToEscapingScheme(cfg.MetricNameEscapingScheme, cfg.MetricNameValidationScheme)
if err != nil {
@ -326,6 +331,11 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error {
sp.config = cfg
oldClient := sp.client
sp.client = client
switch cfg.MetricNameValidationScheme {
case model.LegacyValidation, model.UTF8Validation:
default:
return errors.New("scrapePool.reload: MetricNameValidationScheme must be set in scrape configuration")
}
sp.validationScheme = cfg.MetricNameValidationScheme
var escapingScheme model.EscapingScheme
escapingScheme, err = model.ToEscapingScheme(cfg.MetricNameEscapingScheme)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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