Make metric/label name validation scheme explicit (#16928)
* Parameterize metric/label name validation scheme Parameterized metric/label name validation scheme --------- Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> Co-authored-by: Julius Hinze <julius.hinze@grafana.com>
This commit is contained in:
parent
68d0d3eee3
commit
0a40df33fb
|
@ -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),
|
||||
|
|
|
@ -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)))
|
||||
|
@ -472,13 +472,15 @@ type rulesLintConfig struct {
|
|||
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,
|
||||
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 (*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
|
||||
|
@ -1253,6 +1255,7 @@ func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outpu
|
|||
end: etime,
|
||||
evalInterval: evalInterval,
|
||||
maxBlockDuration: maxBlockDuration,
|
||||
nameValidationScheme: nameValidationScheme,
|
||||
}
|
||||
api, err := newAPI(url, roundTripper, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -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`)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ type ruleImporterConfig struct {
|
|||
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
|
||||
|
@ -63,7 +64,9 @@ func newRuleImporter(logger *slog.Logger, config ruleImporterConfig, apiClient q
|
|||
logger: logger,
|
||||
config: config,
|
||||
apiClient: apiClient,
|
||||
ruleManager: rules.NewManager(&rules.ManagerOptions{}),
|
||||
ruleManager: rules.NewManager(&rules.ManagerOptions{
|
||||
NameValidationScheme: config.nameValidationScheme,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -170,6 +170,7 @@ func newTestRuleImporter(_ context.Context, start time.Time, tmpDir string, test
|
|||
end: start.Add(-7 * time.Hour),
|
||||
evalInterval: 60 * time.Second,
|
||||
maxBlockDuration: maxBlockDuration,
|
||||
nameValidationScheme: model.UTF8Validation,
|
||||
}
|
||||
|
||||
return newRuleImporter(logger, cfg, mockQueryRangeAPI{
|
||||
|
|
|
@ -47,6 +47,7 @@ func TestSDCheckResult(t *testing.T) {
|
|||
TargetLabel: "newfoo",
|
||||
Regex: reg,
|
||||
Replacement: "$1",
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
}},
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
@ -130,6 +133,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.MustNewRegexp("expensive.*"),
|
||||
Replacement: "$1",
|
||||
Action: relabel.Drop,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
QueueConfig: DefaultQueueConfig,
|
||||
|
@ -285,6 +289,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.MustNewRegexp("(.*)some-[regex]"),
|
||||
Replacement: "foo-${1}",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"abc"},
|
||||
|
@ -293,6 +298,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.DefaultRelabelConfig.Regex,
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
TargetLabel: "abc",
|
||||
|
@ -300,6 +306,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.DefaultRelabelConfig.Regex,
|
||||
Replacement: "static",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
TargetLabel: "abc",
|
||||
|
@ -307,6 +314,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.MustNewRegexp(""),
|
||||
Replacement: "static",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"foo"},
|
||||
|
@ -315,6 +323,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.DefaultRelabelConfig.Regex,
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Separator: relabel.DefaultRelabelConfig.Separator,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"foo"},
|
||||
|
@ -323,6 +332,7 @@ var expectedConf = &Config{
|
|||
Regex: relabel.DefaultRelabelConfig.Regex,
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Separator: relabel.DefaultRelabelConfig.Separator,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -382,6 +392,7 @@ var expectedConf = &Config{
|
|||
Separator: ";",
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Action: relabel.Drop,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"__address__"},
|
||||
|
@ -391,6 +402,7 @@ var expectedConf = &Config{
|
|||
Modulus: 8,
|
||||
Separator: ";",
|
||||
Action: relabel.HashMod,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"__tmp_hash"},
|
||||
|
@ -398,24 +410,28 @@ var expectedConf = &Config{
|
|||
Separator: ";",
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Action: relabel.Keep,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
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,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
Regex: relabel.MustNewRegexp("k"),
|
||||
Separator: ";",
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Action: relabel.LabelKeep,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
MetricRelabelConfigs: []*relabel.Config{
|
||||
|
@ -425,6 +441,7 @@ var expectedConf = &Config{
|
|||
Separator: ";",
|
||||
Replacement: relabel.DefaultRelabelConfig.Replacement,
|
||||
Action: relabel.Drop,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -485,6 +502,7 @@ var expectedConf = &Config{
|
|||
TargetLabel: "${1}",
|
||||
Replacement: "${2}",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1282,6 +1300,7 @@ var expectedConf = &Config{
|
|||
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
2
go.mod
|
@ -56,7 +56,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
4
go.sum
|
@ -452,8 +452,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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,18 +765,22 @@ func TestRelabelValidate(t *testing.T) {
|
|||
expected string
|
||||
}{
|
||||
{
|
||||
config: Config{},
|
||||
config: Config{
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
expected: `relabel action cannot be empty`,
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
Action: Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
expected: `requires 'target_label' value`,
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
Action: Lowercase,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
expected: `requires 'target_label' value`,
|
||||
},
|
||||
|
@ -784,8 +789,18 @@ func TestRelabelValidate(t *testing.T) {
|
|||
Action: Lowercase,
|
||||
Replacement: DefaultRelabelConfig.Replacement,
|
||||
TargetLabel: "${3}", // With UTF-8 naming, this is now a legal relabel rule.
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
{
|
||||
config: Config{
|
||||
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"},
|
||||
|
@ -793,6 +808,7 @@ func TestRelabelValidate(t *testing.T) {
|
|||
Action: Replace,
|
||||
Replacement: "${1}",
|
||||
TargetLabel: "${3}",
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -802,6 +818,7 @@ func TestRelabelValidate(t *testing.T) {
|
|||
Action: Replace,
|
||||
Replacement: "${1}",
|
||||
TargetLabel: "0${3}", // With UTF-8 naming this targets a valid label.
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -811,12 +828,13 @@ func TestRelabelValidate(t *testing.T) {
|
|||
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 {
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
"github.com/prometheus/prometheus/template"
|
||||
"github.com/prometheus/prometheus/util/namevalidationutil"
|
||||
)
|
||||
|
||||
// Error represents semantic errors on parsing rule groups.
|
||||
|
@ -96,7 +97,12 @@ 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) {
|
||||
if err := namevalidationutil.CheckNameValidationScheme(nameValidationScheme); err != nil {
|
||||
errs = append(errs, err)
|
||||
return
|
||||
}
|
||||
|
||||
set := map[string]struct{}{}
|
||||
|
||||
for j, g := range g.Groups {
|
||||
|
@ -112,7 +118,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 +134,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 +198,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 +244,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 +261,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 +275,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 +339,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 +364,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])
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
@ -64,6 +67,7 @@ func TestParseFileFailure(t *testing.T) {
|
|||
for _, c := range []struct {
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
groups:
|
||||
- name: yolo
|
||||
rules:
|
||||
- alert: hola
|
||||
expr: 1
|
||||
annotations:
|
||||
ins-tance: localhost
|
|
@ -0,0 +1,7 @@
|
|||
groups:
|
||||
- name: yolo
|
||||
rules:
|
||||
- alert: hola
|
||||
expr: 1
|
||||
annotations:
|
||||
ins_tance: localhost
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
@ -248,6 +248,7 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
|
|||
SourceLabels: model.LabelNames{"alertnamedrop"},
|
||||
Action: "drop",
|
||||
Regex: relabel.MustNewRegexp(".+"),
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -258,6 +259,7 @@ func TestHandlerSendAllRemapPerAm(t *testing.T) {
|
|||
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))
|
||||
|
||||
|
@ -393,9 +395,10 @@ func TestExternalLabels(t *testing.T) {
|
|||
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{
|
||||
|
@ -425,6 +428,7 @@ func TestHandlerRelabel(t *testing.T) {
|
|||
SourceLabels: model.LabelNames{"alertname"},
|
||||
Action: "drop",
|
||||
Regex: relabel.MustNewRegexp("drop"),
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"alertname"},
|
||||
|
@ -432,9 +436,10 @@ func TestHandlerRelabel(t *testing.T) {
|
|||
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1680,7 +1680,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value,
|
|||
|
||||
if e.Op == parser.COUNT_VALUES {
|
||||
valueLabel := e.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 {
|
||||
|
|
|
@ -1587,7 +1587,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))
|
||||
}
|
||||
|
||||
|
@ -1635,12 +1635,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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -776,10 +776,11 @@ func TestSendAlertsDontAffectActiveAlerts(t *testing.T) {
|
|||
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) {
|
||||
|
|
|
@ -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
|
||||
|
@ -135,6 +137,13 @@ type ManagerOptions struct {
|
|||
// NewManager returns an implementation of Manager, ready to be started
|
||||
// by calling the Run method.
|
||||
func NewManager(o *ManagerOptions) *Manager {
|
||||
switch o.NameValidationScheme {
|
||||
case model.UTF8Validation, model.LegacyValidation:
|
||||
case model.UnsetValidation:
|
||||
o.NameValidationScheme = model.UTF8Validation
|
||||
default:
|
||||
panic(fmt.Errorf("unrecognized name validation scheme: %s", o.NameValidationScheme))
|
||||
}
|
||||
if o.Metrics == nil {
|
||||
o.Metrics = NewGroupMetrics(o.Registerer)
|
||||
}
|
||||
|
@ -289,7 +298,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 +306,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 +321,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 +591,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 +611,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...))
|
||||
}
|
||||
|
|
|
@ -810,7 +810,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")
|
||||
|
@ -2552,11 +2552,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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -193,6 +193,7 @@ func TestPopulateLabels(t *testing.T) {
|
|||
SourceLabels: model.LabelNames{"custom"},
|
||||
Replacement: "${1}",
|
||||
TargetLabel: string(model.AddressLabel),
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -231,6 +232,7 @@ func TestPopulateLabels(t *testing.T) {
|
|||
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)
|
||||
|
|
|
@ -54,6 +54,7 @@ import (
|
|||
"github.com/prometheus/prometheus/model/value"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/util/logging"
|
||||
"github.com/prometheus/prometheus/util/namevalidationutil"
|
||||
"github.com/prometheus/prometheus/util/pool"
|
||||
)
|
||||
|
||||
|
@ -154,6 +155,9 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := namevalidationutil.CheckNameValidationScheme(cfg.MetricNameValidationScheme); err != nil {
|
||||
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 +330,9 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error {
|
|||
sp.config = cfg
|
||||
oldClient := sp.client
|
||||
sp.client = client
|
||||
if err := namevalidationutil.CheckNameValidationScheme(cfg.MetricNameValidationScheme); err != nil {
|
||||
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)
|
||||
|
|
|
@ -337,6 +337,7 @@ func TestDroppedTargetsList(t *testing.T) {
|
|||
Action: relabel.Drop,
|
||||
Regex: relabel.MustNewRegexp("dropMe"),
|
||||
SourceLabels: model.LabelNames{"job"},
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1376,6 +1377,7 @@ func TestScrapeLoopFailWithInvalidLabelsAfterRelabel(t *testing.T) {
|
|||
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 {
|
||||
|
@ -4193,6 +4195,7 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
|
|||
Replacement: "3s",
|
||||
TargetLabel: model.ScrapeIntervalLabel,
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{model.ScrapeTimeoutLabel},
|
||||
|
@ -4200,6 +4203,7 @@ func TestTargetScrapeIntervalAndTimeoutRelabel(t *testing.T) {
|
|||
Replacement: "750ms",
|
||||
TargetLabel: model.ScrapeTimeoutLabel,
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -4232,6 +4236,7 @@ func TestLeQuantileReLabel(t *testing.T) {
|
|||
Separator: relabel.DefaultRelabelConfig.Separator,
|
||||
TargetLabel: "le",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"quantile"},
|
||||
|
@ -4240,6 +4245,7 @@ func TestLeQuantileReLabel(t *testing.T) {
|
|||
Separator: relabel.DefaultRelabelConfig.Separator,
|
||||
TargetLabel: "quantile",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
SampleLimit: 100,
|
||||
|
@ -4875,6 +4881,7 @@ func TestTypeUnitReLabel(t *testing.T) {
|
|||
Replacement: "counter",
|
||||
TargetLabel: "__type__",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
SourceLabels: model.LabelNames{"__name__"},
|
||||
|
@ -4882,6 +4889,7 @@ func TestTypeUnitReLabel(t *testing.T) {
|
|||
Replacement: "bytes",
|
||||
TargetLabel: "__unit__",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
SampleLimit: 100,
|
||||
|
@ -5496,6 +5504,7 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
|
|||
SourceLabels: relabel.DefaultRelabelConfig.SourceLabels,
|
||||
TargetLabel: model.ScrapeTimeoutLabel,
|
||||
Replacement: expectedTimeoutLabel,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
Action: relabel.DefaultRelabelConfig.Action,
|
||||
|
@ -5503,6 +5512,7 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
|
|||
SourceLabels: relabel.DefaultRelabelConfig.SourceLabels,
|
||||
TargetLabel: paramLabel,
|
||||
Replacement: expectedParam,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
{
|
||||
Action: relabel.DefaultRelabelConfig.Action,
|
||||
|
@ -5510,6 +5520,7 @@ func TestTargetScrapeConfigWithLabels(t *testing.T) {
|
|||
SourceLabels: relabel.DefaultRelabelConfig.SourceLabels,
|
||||
TargetLabel: model.MetricsPathLabel,
|
||||
Replacement: expectedPath,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -1433,6 +1433,7 @@ func BenchmarkStoreSeries(b *testing.B) {
|
|||
TargetLabel: "job",
|
||||
Replacement: "$1",
|
||||
Action: relabel.Replace,
|
||||
NameValidationScheme: model.UTF8Validation,
|
||||
}}
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
|
|
@ -283,6 +283,7 @@ func TestWriteStorageApplyConfig_PartialUpdate(t *testing.T) {
|
|||
WriteRelabelConfigs: []*relabel.Config{
|
||||
{
|
||||
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{},
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2025 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package namevalidationutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// CheckNameValidationScheme returns an error iff nameValidationScheme is unset.
|
||||
func CheckNameValidationScheme(nameValidationScheme model.ValidationScheme) error {
|
||||
switch nameValidationScheme {
|
||||
case model.UTF8Validation, model.LegacyValidation:
|
||||
case model.UnsetValidation:
|
||||
return errors.New("unset nameValidationScheme")
|
||||
default:
|
||||
panic(fmt.Errorf("unhandled nameValidationScheme: %s", nameValidationScheme.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2025 The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package namevalidationutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCheckNameValidationScheme(t *testing.T) {
|
||||
require.NoError(t, CheckNameValidationScheme(model.UTF8Validation))
|
||||
require.NoError(t, CheckNameValidationScheme(model.LegacyValidation))
|
||||
require.EqualError(t, CheckNameValidationScheme(model.UnsetValidation), "unset nameValidationScheme")
|
||||
require.PanicsWithError(t, "unhandled ValidationScheme: 20", func() {
|
||||
CheckNameValidationScheme(20)
|
||||
})
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue