From 74610b7c89bdc14ebddb1879ebc3c6113e517528 Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Mon, 4 Aug 2025 13:53:50 -0400 Subject: [PATCH] config: address edge case where local config specifies validation mode only (#16923) This check ensures that local ScrapeConfigs that only specify Legacy validation do not inherit the default global AllowUTF8 escaping setting, which is an invalid combination of settings. --------- Signed-off-by: Owen Williams --- CHANGELOG.md | 1 + config/config.go | 16 ++++++- config/config_test.go | 45 ++++++++++++------- .../scrape_config_local_infer_escaping.yml | 3 ++ 4 files changed, 49 insertions(+), 16 deletions(-) create mode 100644 config/testdata/scrape_config_local_infer_escaping.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index adb6b0fe0f..dd3a163c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## main / unreleased * [BUGFIX] OTLP receiver: Generate `target_info` samples between the earliest and latest samples per resource. #16737 +* [BUGFIX] Config: Infer escaping scheme when scrape config validation scheme is set. ## 3.5.0 / 2025-07-14 diff --git a/config/config.go b/config/config.go index 9f65a9a1a7..64dae3e8ac 100644 --- a/config/config.go +++ b/config/config.go @@ -885,8 +885,10 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { return fmt.Errorf("unknown global name validation method specified, must be either '', 'legacy' or 'utf8', got %s", globalConfig.MetricNameValidationScheme) } // Scrapeconfig validation scheme matches global if left blank. + localValidationUnset := false switch c.MetricNameValidationScheme { case model.UnsetValidation: + localValidationUnset = true c.MetricNameValidationScheme = globalConfig.MetricNameValidationScheme case model.LegacyValidation, model.UTF8Validation: default: @@ -906,8 +908,20 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { return fmt.Errorf("unknown global name escaping method specified, must be one of '%s', '%s', '%s', or '%s', got %q", model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues, globalConfig.MetricNameEscapingScheme) } + // Similarly, if ScrapeConfig escaping scheme is blank, infer it from the + // ScrapeConfig validation scheme if that was set, or the Global validation + // scheme if the ScrapeConfig validation scheme was also not set. This ensures + // that local ScrapeConfigs that only specify Legacy validation do not inherit + // the global AllowUTF8 escaping setting, which is an error. if c.MetricNameEscapingScheme == "" { - c.MetricNameEscapingScheme = globalConfig.MetricNameEscapingScheme + //nolint:gocritic + if localValidationUnset { + c.MetricNameEscapingScheme = globalConfig.MetricNameEscapingScheme + } else if c.MetricNameValidationScheme == model.LegacyValidation { + c.MetricNameEscapingScheme = model.EscapeUnderscores + } else { + c.MetricNameEscapingScheme = model.AllowUTF8 + } } switch c.MetricNameEscapingScheme { diff --git a/config/config_test.go b/config/config_test.go index f95f15b1eb..527d3cd319 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2801,29 +2801,40 @@ func TestScrapeConfigDisableCompression(t *testing.T) { func TestScrapeConfigNameValidationSettings(t *testing.T) { tests := []struct { - name string - inputFile string - expectScheme model.ValidationScheme + name string + inputFile string + expectScheme model.ValidationScheme + expectEscaping model.EscapingScheme }{ { - name: "blank config implies default", - inputFile: "scrape_config_default_validation_mode", - expectScheme: model.UTF8Validation, + name: "blank config implies default", + inputFile: "scrape_config_default_validation_mode", + expectScheme: model.UTF8Validation, + expectEscaping: model.NoEscaping, }, { - name: "global setting implies local settings", - inputFile: "scrape_config_global_validation_mode", - expectScheme: model.LegacyValidation, + name: "global setting implies local settings", + inputFile: "scrape_config_global_validation_mode", + expectScheme: model.LegacyValidation, + expectEscaping: model.DotsEscaping, }, { - name: "local setting", - inputFile: "scrape_config_local_validation_mode", - expectScheme: model.LegacyValidation, + name: "local setting", + inputFile: "scrape_config_local_validation_mode", + expectScheme: model.LegacyValidation, + expectEscaping: model.ValueEncodingEscaping, }, { - name: "local setting overrides global setting", - inputFile: "scrape_config_local_global_validation_mode", - expectScheme: model.UTF8Validation, + name: "local setting overrides global setting", + inputFile: "scrape_config_local_global_validation_mode", + expectScheme: model.UTF8Validation, + expectEscaping: model.DotsEscaping, + }, + { + name: "local validation implies underscores escaping", + inputFile: "scrape_config_local_infer_escaping", + expectScheme: model.LegacyValidation, + expectEscaping: model.UnderscoreEscaping, }, } @@ -2839,6 +2850,10 @@ func TestScrapeConfigNameValidationSettings(t *testing.T) { require.NoError(t, yaml.UnmarshalStrict(out, got)) require.Equal(t, tc.expectScheme, got.ScrapeConfigs[0].MetricNameValidationScheme) + + escaping, err := model.ToEscapingScheme(got.ScrapeConfigs[0].MetricNameEscapingScheme) + require.NoError(t, err) + require.Equal(t, tc.expectEscaping, escaping) }) } } diff --git a/config/testdata/scrape_config_local_infer_escaping.yml b/config/testdata/scrape_config_local_infer_escaping.yml new file mode 100644 index 0000000000..90279ff081 --- /dev/null +++ b/config/testdata/scrape_config_local_infer_escaping.yml @@ -0,0 +1,3 @@ +scrape_configs: + - job_name: prometheus + metric_name_validation_scheme: legacy