diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f3e485ed9..51bf616d052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * [FEATURE] Distributor/Ingester: Implemented experimental feature to use gRPC stream connection for push requests. This can be enabled by setting `-distributor.use-stream-push=true`. #6580 * [FEATURE] Compactor: Add support for percentage based sharding for compactors. #6738 * [FEATURE] Querier: Allow choosing PromQL engine via header. #6777 +* [FEATURE] Config: Name validation scheme for metric and label names can be set using the config file (`name_validation_scheme`) as well as a CLI flag (`-name.validation_scheme`) * [ENHANCEMENT] Tenant Federation: Add a # of query result limit logic when the `-tenant-federation.regex-matcher-enabled` is enabled. #6845 * [ENHANCEMENT] Query Frontend: Add a `cortex_slow_queries_total` metric to track # of slow queries per user. #6859 * [ENHANCEMENT] Query Frontend: Change to return 400 when the tenant resolving fail. #6715 diff --git a/docs/configuration/config-file-reference.md b/docs/configuration/config-file-reference.md index 2963a87348c..4294edf1f0b 100644 --- a/docs/configuration/config-file-reference.md +++ b/docs/configuration/config-file-reference.md @@ -68,6 +68,12 @@ Where default_value is the value to use if the environment variable is undefined # CLI flag: -http.prefix [http_prefix: | default = "/api/prom"] +# Validation scheme for metric and label names. +# Set to "utf8" to allow UTF-8 characters in metric and label names. +# Set to "legacy" to enforce strict legacy-compatible name rules. +# CLI flag: -name.validation_scheme +[name_validation_scheme: | default = "legacy"] + resource_monitor: # Comma-separated list of resources to monitor. Supported values are cpu and # heap, which tracks metrics from github.com/prometheus/procfs and diff --git a/go.mod b/go.mod index 1b88330c581..535d4c2e0ca 100644 --- a/go.mod +++ b/go.mod @@ -43,9 +43,9 @@ require ( github.com/prometheus/alertmanager v0.28.1 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.63.0 + github.com/prometheus/common v0.65.0 // Prometheus maps version 2.x.y to tags v0.x.y. - github.com/prometheus/prometheus v0.303.1 + github.com/prometheus/prometheus v1.99.0 github.com/segmentio/fasthash v1.0.3 github.com/sony/gobreaker v1.0.0 github.com/spf13/afero v1.11.0 diff --git a/go.sum b/go.sum index 9aa18e3ac6f..f333c5a06d2 100644 --- a/go.sum +++ b/go.sum @@ -838,8 +838,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg= github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= diff --git a/pkg/cortex/cortex.go b/pkg/cortex/cortex.go index b141adc8127..378e5a63774 100644 --- a/pkg/cortex/cortex.go +++ b/pkg/cortex/cortex.go @@ -14,6 +14,8 @@ import ( "github.com/go-kit/log/level" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/model" + prom_config "github.com/prometheus/prometheus/config" "github.com/prometheus/prometheus/promql" prom_storage "github.com/prometheus/prometheus/storage" "github.com/weaveworks/common/server" @@ -90,11 +92,12 @@ var ( // Config is the root config for Cortex. type Config struct { - Target flagext.StringSliceCSV `yaml:"target"` - AuthEnabled bool `yaml:"auth_enabled"` - PrintConfig bool `yaml:"-"` - HTTPPrefix string `yaml:"http_prefix"` - ResourceMonitor configs.ResourceMonitor `yaml:"resource_monitor"` + Target flagext.StringSliceCSV `yaml:"target"` + AuthEnabled bool `yaml:"auth_enabled"` + PrintConfig bool `yaml:"-"` + HTTPPrefix string `yaml:"http_prefix"` + ResourceMonitor configs.ResourceMonitor `yaml:"resource_monitor"` + NameValidationScheme string `yaml:"name_validation_scheme"` ExternalQueryable prom_storage.Queryable `yaml:"-"` ExternalPusher ruler.Pusher `yaml:"-"` @@ -146,7 +149,7 @@ func (c *Config) RegisterFlags(f *flag.FlagSet) { f.BoolVar(&c.AuthEnabled, "auth.enabled", true, "Set to false to disable auth.") f.BoolVar(&c.PrintConfig, "print.config", false, "Print the config and exit.") f.StringVar(&c.HTTPPrefix, "http.prefix", "/api/prom", "HTTP path prefix for Cortex API.") - + f.StringVar(&c.NameValidationScheme, "name.validation_scheme", "legacy", "Validation scheme for metric and label names. Set to utf8 to allow UTF-8 characters.") c.API.RegisterFlags(f) c.registerServerFlagsWithChangedDefaultValues(f) c.Distributor.RegisterFlags(f) @@ -181,6 +184,11 @@ func (c *Config) RegisterFlags(f *flag.FlagSet) { // Validate the cortex config and returns an error if the validation // doesn't pass func (c *Config) Validate(log log.Logger) error { + switch c.NameValidationScheme { + case "", prom_config.LegacyValidationConfig, prom_config.UTF8ValidationConfig: + default: + return fmt.Errorf("invalid name validation scheme: %s", c.NameValidationScheme) + } if err := c.validateYAMLEmptyNodes(); err != nil { return err } @@ -349,7 +357,12 @@ func New(cfg Config) (*Cortex, error) { } os.Exit(0) } - + //nolint:staticcheck // SA1019: intentional use of deprecated NameValidationScheme for backward compatibility + if cfg.NameValidationScheme == prom_config.UTF8ValidationConfig { + model.NameValidationScheme = model.UTF8Validation + } else { + model.NameValidationScheme = model.LegacyValidation + } // Swap out the default resolver to support multiple tenant IDs separated by a '|' if cfg.TenantFederation.Enabled { util_log.WarnExperimentalUse("tenant-federation") diff --git a/pkg/cortex/cortex_test.go b/pkg/cortex/cortex_test.go index 6cac224319d..8cc466932fc 100644 --- a/pkg/cortex/cortex_test.go +++ b/pkg/cortex/cortex_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + prom_config "github.com/prometheus/prometheus/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/weaveworks/common/server" @@ -217,6 +218,42 @@ func TestConfigValidation(t *testing.T) { }, expectedError: nil, }, + { + name: "should not fail validation for empty name validation scheme (use legacy by default)", + getTestConfig: func() *Config { + configuration := newDefaultConfig() + configuration.NameValidationScheme = "" + return configuration + }, + expectedError: nil, + }, + { + name: "should not fail validation for legacy name validation scheme", + getTestConfig: func() *Config { + configuration := newDefaultConfig() + configuration.NameValidationScheme = prom_config.LegacyValidationConfig + return configuration + }, + expectedError: nil, + }, + { + name: "should not fail validation for utf-8 name validation scheme", + getTestConfig: func() *Config { + configuration := newDefaultConfig() + configuration.NameValidationScheme = prom_config.UTF8ValidationConfig + return configuration + }, + expectedError: nil, + }, + { + name: "should fail validation for invalid name validation scheme", + getTestConfig: func() *Config { + configuration := newDefaultConfig() + configuration.NameValidationScheme = "invalid" + return configuration + }, + expectedError: fmt.Errorf("invalid name validation scheme"), + }, } { t.Run(tc.name, func(t *testing.T) { err := tc.getTestConfig().Validate(nil) diff --git a/vendor/github.com/prometheus/common/config/http_config.go b/vendor/github.com/prometheus/common/config/http_config.go index 63809083aca..5d3f1941bb0 100644 --- a/vendor/github.com/prometheus/common/config/http_config.go +++ b/vendor/github.com/prometheus/common/config/http_config.go @@ -225,7 +225,7 @@ func (u *URL) UnmarshalJSON(data []byte) error { // MarshalJSON implements the json.Marshaler interface for URL. func (u URL) MarshalJSON() ([]byte, error) { if u.URL != nil { - return json.Marshal(u.URL.String()) + return json.Marshal(u.String()) } return []byte("null"), nil } @@ -251,7 +251,7 @@ func (o *OAuth2) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal((*plain)(o)); err != nil { return err } - return o.ProxyConfig.Validate() + return o.Validate() } // UnmarshalJSON implements the json.Marshaler interface for URL. @@ -260,7 +260,7 @@ func (o *OAuth2) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, (*plain)(o)); err != nil { return err } - return o.ProxyConfig.Validate() + return o.Validate() } // SetDirectory joins any relative file paths with dir. @@ -604,8 +604,8 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon // The only timeout we care about is the configured scrape timeout. // It is applied on request. So we leave out any timings here. var rt http.RoundTripper = &http.Transport{ - Proxy: cfg.ProxyConfig.Proxy(), - ProxyConnectHeader: cfg.ProxyConfig.GetProxyConnectHeader(), + Proxy: cfg.Proxy(), + ProxyConnectHeader: cfg.GetProxyConnectHeader(), MaxIdleConns: 20000, MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801 DisableKeepAlives: !opts.keepAlivesEnabled, @@ -914,8 +914,8 @@ func (rt *oauth2RoundTripper) newOauth2TokenSource(req *http.Request, secret str tlsTransport := func(tlsConfig *tls.Config) (http.RoundTripper, error) { return &http.Transport{ TLSClientConfig: tlsConfig, - Proxy: rt.config.ProxyConfig.Proxy(), - ProxyConnectHeader: rt.config.ProxyConfig.GetProxyConnectHeader(), + Proxy: rt.config.Proxy(), + ProxyConnectHeader: rt.config.GetProxyConnectHeader(), DisableKeepAlives: !rt.opts.keepAlivesEnabled, MaxIdleConns: 20, MaxIdleConnsPerHost: 1, // see https://github.com/golang/go/issues/13801 @@ -1508,7 +1508,7 @@ func (c *ProxyConfig) Proxy() (fn func(*http.Request) (*url.URL, error)) { } return } - if c.ProxyURL.URL != nil && c.ProxyURL.URL.String() != "" { + if c.ProxyURL.URL != nil && c.ProxyURL.String() != "" { if c.NoProxy == "" { c.proxyFunc = http.ProxyURL(c.ProxyURL.URL) return diff --git a/vendor/github.com/prometheus/common/expfmt/text_parse.go b/vendor/github.com/prometheus/common/expfmt/text_parse.go index b4607fe4d27..4067978a178 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_parse.go +++ b/vendor/github.com/prometheus/common/expfmt/text_parse.go @@ -345,8 +345,8 @@ func (p *TextParser) startLabelName() stateFn { } // Special summary/histogram treatment. Don't add 'quantile' and 'le' // labels to 'real' labels. - if !(p.currentMF.GetType() == dto.MetricType_SUMMARY && p.currentLabelPair.GetName() == model.QuantileLabel) && - !(p.currentMF.GetType() == dto.MetricType_HISTOGRAM && p.currentLabelPair.GetName() == model.BucketLabel) { + if (p.currentMF.GetType() != dto.MetricType_SUMMARY || p.currentLabelPair.GetName() != model.QuantileLabel) && + (p.currentMF.GetType() != dto.MetricType_HISTOGRAM || p.currentLabelPair.GetName() != model.BucketLabel) { p.currentLabelPairs = append(p.currentLabelPairs, p.currentLabelPair) } // Check for duplicate label names. diff --git a/vendor/github.com/prometheus/common/model/labels.go b/vendor/github.com/prometheus/common/model/labels.go index f4a387605f1..de83afe93e9 100644 --- a/vendor/github.com/prometheus/common/model/labels.go +++ b/vendor/github.com/prometheus/common/model/labels.go @@ -122,7 +122,8 @@ func (ln LabelName) IsValidLegacy() bool { return false } for i, b := range ln { - if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { + // TODO: Apply De Morgan's law. Make sure there are tests for this. + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { //nolint:staticcheck return false } } diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go index 5727452c1ee..fed9e87b915 100644 --- a/vendor/github.com/prometheus/common/model/time.go +++ b/vendor/github.com/prometheus/common/model/time.go @@ -201,6 +201,7 @@ var unitMap = map[string]struct { // ParseDuration parses a string into a time.Duration, assuming that a year // always has 365d, a week always has 7d, and a day always has 24h. +// Negative durations are not supported. func ParseDuration(s string) (Duration, error) { switch s { case "0": @@ -253,18 +254,36 @@ func ParseDuration(s string) (Duration, error) { return 0, errors.New("duration out of range") } } + return Duration(dur), nil } +// ParseDurationAllowNegative is like ParseDuration but also accepts negative durations. +func ParseDurationAllowNegative(s string) (Duration, error) { + if s == "" || s[0] != '-' { + return ParseDuration(s) + } + + d, err := ParseDuration(s[1:]) + + return -d, err +} + func (d Duration) String() string { var ( - ms = int64(time.Duration(d) / time.Millisecond) - r = "" + ms = int64(time.Duration(d) / time.Millisecond) + r = "" + sign = "" ) + if ms == 0 { return "0s" } + if ms < 0 { + sign, ms = "-", -ms + } + f := func(unit string, mult int64, exact bool) { if exact && ms%mult != 0 { return @@ -286,7 +305,7 @@ func (d Duration) String() string { f("s", 1000, false) f("ms", 1, false) - return r + return sign + r } // MarshalJSON implements the json.Marshaler interface. diff --git a/vendor/github.com/prometheus/common/promslog/slog.go b/vendor/github.com/prometheus/common/promslog/slog.go index f9f89966315..8da43aef527 100644 --- a/vendor/github.com/prometheus/common/promslog/slog.go +++ b/vendor/github.com/prometheus/common/promslog/slog.go @@ -76,6 +76,11 @@ func (l *Level) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// Level returns the value of the logging level as an slog.Level. +func (l *Level) Level() slog.Level { + return l.lvl.Level() +} + // String returns the current level. func (l *Level) String() string { switch l.lvl.Level() { @@ -200,9 +205,8 @@ func defaultReplaceAttr(_ []string, a slog.Attr) slog.Attr { key := a.Key switch key { case slog.TimeKey: - if t, ok := a.Value.Any().(time.Time); ok { - a.Value = slog.TimeValue(t.UTC()) - } else { + // Note that we do not change the timezone to UTC anymore. + if _, ok := a.Value.Any().(time.Time); !ok { // If we can't cast the any from the value to a // time.Time, it means the caller logged // another attribute with a key of `time`. @@ -267,5 +271,5 @@ func New(config *Config) *slog.Logger { // NewNopLogger is a convenience function to return an slog.Logger that writes // to io.Discard. func NewNopLogger() *slog.Logger { - return slog.New(slog.NewTextHandler(io.Discard, nil)) + return New(&Config{Writer: io.Discard}) } diff --git a/vendor/modules.txt b/vendor/modules.txt index ca8316be582..142d5c5662a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1035,8 +1035,8 @@ github.com/prometheus/client_golang/prometheus/testutil/promlint/validations # github.com/prometheus/client_model v0.6.2 ## explicit; go 1.22.0 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.63.0 -## explicit; go 1.21 +# github.com/prometheus/common v0.65.0 +## explicit; go 1.23.0 github.com/prometheus/common/config github.com/prometheus/common/expfmt github.com/prometheus/common/helpers/templates @@ -1052,7 +1052,7 @@ github.com/prometheus/exporter-toolkit/web github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util -# github.com/prometheus/prometheus v0.303.1 => github.com/thanos-io/thanos-prometheus v0.0.0-20250610133519-082594458a88 +# github.com/prometheus/prometheus v1.99.0 => github.com/thanos-io/thanos-prometheus v0.0.0-20250610133519-082594458a88 ## explicit; go 1.23.0 github.com/prometheus/prometheus/config github.com/prometheus/prometheus/discovery