diff --git a/.golangci.yml b/.golangci.yml index 2a37d5be725..ebd5388b75d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,9 +47,16 @@ linters: comparison: true forbidigo: # We can't use faillint for a rule like this, because it does not support matching methods on structs or interfaces (see https://github.com/fatih/faillint/issues/18) + analyze-types: true forbid: - pattern: ^.*\.CloseSend.*$ msg: Do not use CloseSend on a server-streaming gRPC stream. Use util.CloseAndExhaust instead. See the documentation on CloseAndExhaust for further explanation. + - pattern: ^.*model\.LabelName\.IsValid$ + pkg: ^github.com/prometheus/common/model$ + msg: it depends on model.NameValidationScheme. Use pkg/validation.IsValidLabelName instead. + - pattern: ^.*model\.IsValidMetricName$ + pkg: ^github.com/prometheus/common/model$ + msg: it depends on model.NameValidationScheme. Use pkg/validation.IsValidMetricName instead. gocritic: disable-all: true enabled-checks: diff --git a/go.mod b/go.mod index 744d1c4047f..a5b985d6371 100644 --- a/go.mod +++ b/go.mod @@ -344,7 +344,7 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -replace github.com/prometheus/prometheus => github.com/grafana/mimir-prometheus v1.8.2-0.20250611134813-2510b5041da2 +replace github.com/prometheus/prometheus => github.com/juliusmh/mimir-prometheus v1.8.2-0.20250618091651-809285202792 // Replace memberlist with our fork which includes some fixes that haven't been // merged upstream yet: diff --git a/go.sum b/go.sum index 85f5e17b45a..a3c5bb9e1c2 100644 --- a/go.sum +++ b/go.sum @@ -573,8 +573,6 @@ github.com/grafana/memberlist v0.3.1-0.20250428154222-f7d51a6f6700 h1:0t7iOQ5ZkB github.com/grafana/memberlist v0.3.1-0.20250428154222-f7d51a6f6700/go.mod h1:Ri9p/tRShbjYnpNf4FFPXG7wxEGY4Nrcn6E7jrVa//4= github.com/grafana/mimir-otlptranslator v0.0.0-20250527173959-2573485683d5 h1:H4Del07v9UAYJgky9oeRY1oNqs6dh8lmHRUWiGCKXoE= github.com/grafana/mimir-otlptranslator v0.0.0-20250527173959-2573485683d5/go.mod h1:v1PzmPjSnNkmZSDvKJ9OmsWcmWMEF5+JdllEcXrRfzM= -github.com/grafana/mimir-prometheus v1.8.2-0.20250611134813-2510b5041da2 h1:MJTw9VpZ2I65F5JkF4tUhRjskq9LmR5W0dmJUkBT3zY= -github.com/grafana/mimir-prometheus v1.8.2-0.20250611134813-2510b5041da2/go.mod h1:wxNDaTwYlAYXHIlsXbCj1xQZik2CB+GGqYW6LUJq6s4= github.com/grafana/opentracing-contrib-go-stdlib v0.0.0-20230509071955-f410e79da956 h1:em1oddjXL8c1tL0iFdtVtPloq2hRPen2MJQKoAWpxu0= github.com/grafana/opentracing-contrib-go-stdlib v0.0.0-20230509071955-f410e79da956/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= @@ -715,6 +713,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/juliusmh/mimir-prometheus v1.8.2-0.20250618091651-809285202792 h1:Tk/0EGEqvUBs0H5gNmy23wDaFKUpZu4p5p0asR97o6Q= +github.com/juliusmh/mimir-prometheus v1.8.2-0.20250618091651-809285202792/go.mod h1:wxNDaTwYlAYXHIlsXbCj1xQZik2CB+GGqYW6LUJq6s4= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= diff --git a/integration/legacy_name_validation_test.go b/integration/legacy_name_validation_test.go new file mode 100644 index 00000000000..16722e60d17 --- /dev/null +++ b/integration/legacy_name_validation_test.go @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/integration/alertmanager_test.go +// Provenance-includes-license: Apache-2.0 +// Provenance-includes-copyright: The Cortex Authors. +//go:build requires_docker + +package integration + +import ( + "net/http" + "path/filepath" + "testing" + "time" + + "github.com/grafana/e2e" + e2edb "github.com/grafana/e2e/db" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/rulefmt" + "github.com/prometheus/prometheus/prompb" + promRW2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" + "github.com/stretchr/testify/require" + + "github.com/grafana/mimir/integration/e2emimir" +) + +// TestLegacyNameValidation_DistributorWrite ensures that distributors discard invalid +// metric and label names in the write path. +func TestLegacyNameValidation_DistributorWrite(t *testing.T) { + s, err := e2e.NewScenario(networkName) + require.NoError(t, err) + defer s.Close() + + require.NoError(t, writeFileToSharedDir(s, "runtime.yaml", []byte(""))) + + consul := e2edb.NewConsul() + minio := e2edb.NewMinio(9000, blocksBucketName) + require.NoError(t, s.StartAndWaitReady(consul, minio)) + + baseFlags := map[string]string{ + "-distributor.ingestion-tenant-shard-size": "0", + "-ingester.ring.heartbeat-period": "1s", + "-distributor.ha-tracker.enable": "true", + "-distributor.ha-tracker.enable-for-all-users": "true", + "-distributor.ha-tracker.store": "consul", + "-distributor.ha-tracker.consul.hostname": consul.NetworkHTTPEndpoint(), + "-distributor.ha-tracker.prefix": "prom_ha/", + "-timeseries-unmarshal-caching-optimization-enabled": "false", + } + + flags := mergeFlags( + BlocksStorageFlags(), + BlocksStorageS3Flags(), + baseFlags, + ) + + // We want only distributor to be reloading runtime config. + distributorFlags := mergeFlags(flags, map[string]string{ + "-runtime-config.file": filepath.Join(e2e.ContainerSharedDir, "runtime.yaml"), + "-runtime-config.reload-period": "100ms", + // Set non-zero default for number of exemplars. That way our values used in the test (0 and 100) will show up in runtime config diff. + "-ingester.max-global-exemplars-per-user": "3", + }) + + // Ingester will not reload runtime config. + ingesterFlags := mergeFlags(flags, map[string]string{ + // Ingester will always see exemplars enabled. We do this to avoid waiting for ingester to apply new setting to TSDB. + "-ingester.max-global-exemplars-per-user": "100", + }) + + // Start Mimir components. + distributor := e2emimir.NewDistributor("distributor", consul.NetworkHTTPEndpoint(), distributorFlags) + ingester := e2emimir.NewIngester("ingester", consul.NetworkHTTPEndpoint(), ingesterFlags) + querier := e2emimir.NewQuerier("querier", consul.NetworkHTTPEndpoint(), flags) + require.NoError(t, s.StartAndWaitReady(distributor, ingester, querier)) + + // Wait until distributor has updated the ring. + require.NoError(t, distributor.WaitSumMetricsWithOptions(e2e.Equals(1), []string{"cortex_ring_members"}, e2e.WithLabelMatchers( + labels.MustNewMatcher(labels.MatchEqual, "name", "ingester"), + labels.MustNewMatcher(labels.MatchEqual, "state", "ACTIVE")))) + + // Wait until querier has updated the ring. + require.NoError(t, querier.WaitSumMetricsWithOptions(e2e.Equals(1), []string{"cortex_ring_members"}, e2e.WithLabelMatchers( + labels.MustNewMatcher(labels.MatchEqual, "name", "ingester"), + labels.MustNewMatcher(labels.MatchEqual, "state", "ACTIVE")))) + + client, err := e2emimir.NewClient(distributor.HTTPEndpoint(), querier.HTTPEndpoint(), "", "", userID) + require.NoError(t, err) + + now := time.Now().Truncate(time.Millisecond).UnixMilli() + + testCases := []struct { + name string + rw1request *prompb.WriteRequest + rw2request *promRW2.Request + }{ + { + name: "utf8 metric name", + rw1request: &prompb.WriteRequest{ + Timeseries: []prompb.TimeSeries{ + { + Labels: []prompb.Label{{Name: "__name__", Value: "utf_metric😀C"}}, + Samples: []prompb.Sample{{Timestamp: now, Value: 100}}, + }, + }, + Metadata: []prompb.MetricMetadata{ + { + MetricFamilyName: "utf_metric😀C_total", + Help: "some helpC", + Unit: "someunitC", + Type: prompb.MetricMetadata_COUNTER, + }, + }, + }, + rw2request: &promRW2.Request{ + Timeseries: []promRW2.TimeSeries{ + { + LabelsRefs: []uint32{0, 1}, + Samples: []promRW2.Sample{{Timestamp: now, Value: 100}}, + Metadata: promRW2.Metadata{ + Type: promRW2.Metadata_METRIC_TYPE_COUNTER, + HelpRef: 2, + UnitRef: 3, + }, + }, + }, + Symbols: []string{ + "__name__", "utf_metric😀C_total", + "some helpC", + "someunitC", + }, + }, + }, + { + name: "utf8 label name", + rw1request: &prompb.WriteRequest{ + Timeseries: []prompb.TimeSeries{ + { + Labels: []prompb.Label{ + {Name: "__name__", Value: "legacy_metricC"}, + {Name: "utf8_label😀", Value: "test"}, + }, + Samples: []prompb.Sample{{Timestamp: now, Value: 100}}, + }, + }, + }, + rw2request: &promRW2.Request{ + Timeseries: []promRW2.TimeSeries{ + { + LabelsRefs: []uint32{0, 1, 2, 3}, + Samples: []promRW2.Sample{{Timestamp: now, Value: 100}}, + }, + }, + Symbols: []string{ + "__name__", "legacy_metricC_total", + "utf8_label😀", "test", + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("rw1", func(t *testing.T) { + res, err := client.PushRW1(tc.rw1request) + require.NoError(t, err) + require.Equal(t, res.StatusCode, http.StatusBadRequest) + }) + t.Run("rw2", func(t *testing.T) { + res, err := client.PushRW2(tc.rw2request) + require.NoError(t, err) + require.Equal(t, res.StatusCode, http.StatusBadRequest) + }) + }) + } +} + +// TestLegacyNameValidation_Read ensures that Mimir discards queries containing +// invalid metric and label names. +func TestLegacyNameValidation_Querier(t *testing.T) { + for _, queryEngine := range []string{"mimir", "prometheus"} { + t.Run(queryEngine, func(t *testing.T) { + testLegacyNameValidationQuerier(t, queryEngine) + }) + } +} + +func testLegacyNameValidationQuerier(t *testing.T, queryEngine string) { + s, err := e2e.NewScenario(networkName) + require.NoError(t, err) + defer s.Close() + + consul := e2edb.NewConsul() + minio := e2edb.NewMinio(9000, blocksBucketName) + require.NoError(t, s.StartAndWaitReady(consul, minio)) + + // Configure the blocks storage to frequently compact TSDB head + // and ship blocks to the storage. + const blockRangePeriod = 5 * time.Second + flags := mergeFlags(BlocksStorageFlags(), BlocksStorageS3Flags(), map[string]string{ + "-blocks-storage.tsdb.block-ranges-period": blockRangePeriod.String(), + "-blocks-storage.tsdb.ship-interval": "1s", + "-blocks-storage.bucket-store.sync-interval": "1s", + "-blocks-storage.tsdb.retention-period": ((blockRangePeriod * 2) - 1).String(), + "-querier.max-fetched-series-per-query": "3", + "-querier.query-engine": queryEngine, + }) + + // Start Mimir components. + distributor := e2emimir.NewDistributor("distributor", consul.NetworkHTTPEndpoint(), flags) + ingester := e2emimir.NewIngester("ingester", consul.NetworkHTTPEndpoint(), flags) + storeGateway := e2emimir.NewStoreGateway("store-gateway", consul.NetworkHTTPEndpoint(), flags) + require.NoError(t, s.StartAndWaitReady(distributor, ingester, storeGateway)) + + querier := e2emimir.NewQuerier("querier", consul.NetworkHTTPEndpoint(), flags) + require.NoError(t, s.StartAndWaitReady(querier)) // Wait until distributor has updated the ring. + + client, err := e2emimir.NewClient(distributor.HTTPEndpoint(), querier.HTTPEndpoint(), "", "", userID) + require.NoError(t, err) + + now := time.Now().Truncate(time.Millisecond) + + // Initialize mimir with some test data: + res, err := client.PushRW2(&promRW2.Request{ + Timeseries: []promRW2.TimeSeries{ + { + LabelsRefs: []uint32{0, 1, 2, 3}, + Samples: []promRW2.Sample{{Timestamp: now.UnixMilli(), Value: 100}}, + }, + }, + Symbols: []string{ + "__name__", "legacy_metricC_total", + "label_name", "label_value", + }, + }) + require.NoError(t, err) + require.Equal(t, res.StatusCode, http.StatusOK) + + testCases := []struct { + name string + query string + wantErr string + }{ + { + // this passes always, no matter model.NameValidationScheme + name: "utf8 metric name", + query: `{"invalid_metric😀C_total"}`, + }, + { + // this passes always, no matter model.NameValidationScheme + name: "utf8 label name", + query: `legacy_metricC_total{"😀"="test"}`, + }, + { + // Invalid promql: + // see: https://prometheus.io/docs/guides/utf8/ + name: "invalid promql syntax", + query: `invalid_metric😀C_total`, + wantErr: `bad_data: invalid parameter "query": 1:15: parse error: unexpected character: '😀'`, + }, + { + name: "utf8 label_join", + query: `label_join(legacy_metricC_total, "invalid😀", "-", "label_name")`, + wantErr: `execution: invalid destination label name in label_join(): invalid😀`, + }, + { + name: "utf8 label_replace", + query: `label_replace(legacy_metricC_total, "invalid😀", "$1", "label_name", "(.*):.*")`, + wantErr: `execution: invalid destination label name in label_replace(): invalid😀`, + }, + { + name: "utf8 count_values", + query: `count_values("invalid😀", series)`, + wantErr: `execution: invalid label name "invalid😀"`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := client.Query(tc.query, now) + if tc.wantErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.wantErr) + } + }) + } +} + +// TestLegacyNameValidation_Ruler ensures rule only accepts rules that are +// adhere to legacy naming conventions +func TestLegacyNameValidation_Ruler(t *testing.T) { + s, err := e2e.NewScenario(networkName) + require.NoError(t, err) + defer s.Close() + + // Start dependencies. + consul := e2edb.NewConsul() + minio := e2edb.NewMinio(9000, mimirBucketName, rulesBucketName) + require.NoError(t, s.StartAndWaitReady(consul, minio)) + + // Configure the ruler. + rulerFlags := mergeFlags(CommonStorageBackendFlags(), RulerFlags(), RulerStorageS3Flags(), BlocksStorageFlags()) + + // Start Mimir components. + ruler := e2emimir.NewRuler("ruler", consul.NetworkHTTPEndpoint(), rulerFlags) + require.NoError(t, s.StartAndWaitReady(ruler)) + + // Create a client with the ruler address configured + c, err := e2emimir.NewClient("", "", "", ruler.HTTPEndpoint(), "user-1") + require.NoError(t, err) + + const namespace = "test_/encoded_+namespace/?" + testCases := []struct { + name string + rg rulefmt.RuleGroup + }{ + { + name: "utf8 rule group label nanme", + rg: rulefmt.RuleGroup{ + Name: "test", + Rules: []rulefmt.Rule{ + { + Record: "test", + }, + }, + Labels: map[string]string{ + "invalid😀": "test", + }, + }, + }, + { + name: "utf8 recording rule metric name", + rg: rulefmt.RuleGroup{ + Name: "test", + Rules: []rulefmt.Rule{ + { + Record: "invalid😀", + }, + }, + }, + }, + { + name: "utf8 rule label name", + rg: rulefmt.RuleGroup{ + Name: "test", + Rules: []rulefmt.Rule{ + { + Record: "valid", + Labels: map[string]string{ + "invalid😀": "test", + }, + }, + }, + }, + }, + { + name: "utf8 rule annotations", + rg: rulefmt.RuleGroup{ + Name: "test", + Rules: []rulefmt.Rule{ + { + Record: "valid", + Annotations: map[string]string{ + "invalid😀": "test", + }, + }, + }, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := c.SetRuleGroup(tc.rg, namespace) + require.EqualError(t, err, `unexpected status code: 400`) + }) + } + +} diff --git a/pkg/api/handlers.go b/pkg/api/handlers.go index 50f02e61556..d69e7b285d6 100644 --- a/pkg/api/handlers.go +++ b/pkg/api/handlers.go @@ -27,6 +27,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/route" "github.com/prometheus/prometheus/config" + prom_validation "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/storage" v1 "github.com/prometheus/prometheus/web/api/v1" @@ -287,6 +288,7 @@ func NewQuerierHandler( 0, ) + api.NamingScheme = prom_validation.LegacyNamingScheme api.InstallCodec(protobufCodec{}) router := mux.NewRouter() diff --git a/pkg/cardinality/request.go b/pkg/cardinality/request.go index b42db2e5984..b614689e90a 100644 --- a/pkg/cardinality/request.go +++ b/pkg/cardinality/request.go @@ -14,6 +14,8 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" + + "github.com/grafana/mimir/pkg/util/validation" ) type CountMethod string @@ -263,7 +265,7 @@ func extractLabelNames(values url.Values) ([]model.LabelName, error) { labelNames := make([]model.LabelName, 0, len(labelNamesParams)) for _, labelNameParam := range labelNamesParams { labelName := model.LabelName(labelNameParam) - if !labelName.IsValid() { + if !validation.IsValidLabelName(labelNameParam) { return nil, fmt.Errorf("invalid 'label_names' param '%v'", labelNameParam) } labelNames = append(labelNames, labelName) diff --git a/pkg/distributor/distributor.go b/pkg/distributor/distributor.go index 72a1381829f..55b8a6dea8d 100644 --- a/pkg/distributor/distributor.go +++ b/pkg/distributor/distributor.go @@ -67,12 +67,6 @@ import ( var tracer = otel.Tracer("pkg/distributor") -func init() { - // Mimir doesn't support Prometheus' UTF-8 metric/label name scheme yet. - // nolint:staticcheck - model.NameValidationScheme = model.LegacyValidation -} - var ( // Validation errors. errInvalidTenantShardSize = errors.New("invalid tenant shard size, the value must be greater than or equal to zero") diff --git a/pkg/distributor/validate.go b/pkg/distributor/validate.go index 65947468364..a621e468084 100644 --- a/pkg/distributor/validate.go +++ b/pkg/distributor/validate.go @@ -410,7 +410,7 @@ func validateLabels(m *sampleValidationMetrics, cfg labelValidationConfig, userI return errors.New(noMetricNameMsgFormat) } - if !model.IsValidMetricName(model.LabelValue(unsafeMetricName)) { + if !validation.IsValidMetricName(unsafeMetricName) { cat.IncrementDiscardedSamples(ls, 1, reasonInvalidMetricName, ts) m.invalidMetricName.WithLabelValues(userID, group).Inc() return fmt.Errorf(invalidMetricNameMsgFormat, removeNonASCIIChars(unsafeMetricName)) @@ -436,7 +436,7 @@ func validateLabels(m *sampleValidationMetrics, cfg labelValidationConfig, userI maxLabelValueLength := cfg.MaxLabelValueLength(userID) lastLabelName := "" for _, l := range ls { - if !skipLabelValidation && !model.LabelName(l.Name).IsValid() { + if !skipLabelValidation && !validation.IsValidLabelName(l.Name) { m.invalidLabel.WithLabelValues(userID, group).Inc() cat.IncrementDiscardedSamples(ls, 1, reasonInvalidLabel, ts) return fmt.Errorf(invalidLabelMsgFormat, l.Name, mimirpb.FromLabelAdaptersToString(ls)) diff --git a/pkg/frontend/querymiddleware/request_validation.go b/pkg/frontend/querymiddleware/request_validation.go index 6db11683dc0..3d05c5f2f74 100644 --- a/pkg/frontend/querymiddleware/request_validation.go +++ b/pkg/frontend/querymiddleware/request_validation.go @@ -7,15 +7,8 @@ import ( "net/http" "github.com/grafana/dskit/cancellation" - "github.com/prometheus/common/model" ) -func init() { - // Mimir doesn't support Prometheus' UTF-8 metric/label name scheme yet. - // nolint:staticcheck - model.NameValidationScheme = model.LegacyValidation -} - const requestValidationFailedFmt = "request validation failed for " var errMetricsQueryRequestValidationFailed = cancellation.NewErrorf( diff --git a/pkg/mimir/promexts.go b/pkg/mimir/promexts.go index cabc1122543..79356aaf6d9 100644 --- a/pkg/mimir/promexts.go +++ b/pkg/mimir/promexts.go @@ -3,14 +3,9 @@ package mimir import ( - "github.com/prometheus/common/model" - "github.com/grafana/mimir/pkg/util/promqlext" ) func init() { promqlext.ExtendPromQL() - // Mimir doesn't support Prometheus' UTF-8 metric/label name scheme yet. - // nolint:staticcheck - model.NameValidationScheme = model.LegacyValidation } diff --git a/pkg/mimirtool/rules/rules.go b/pkg/mimirtool/rules/rules.go index 71de931eb76..fa571de9a2a 100644 --- a/pkg/mimirtool/rules/rules.go +++ b/pkg/mimirtool/rules/rules.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/prometheus/prometheus/model/rulefmt" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql/parser" log "github.com/sirupsen/logrus" @@ -255,7 +256,7 @@ func (r RuleNamespace) Validate(groupNodes []rulefmt.RuleGroupNode) []error { func ValidateRuleGroup(g rwrulefmt.RuleGroup, node rulefmt.RuleGroupNode) []error { var errs []error for i, r := range g.Rules { - for _, err := range r.Validate(node.Rules[i]) { + for _, err := range r.Validate(node.Rules[i], validation.LegacyNamingScheme) { var ruleName string if r.Alert != "" { ruleName = r.Alert diff --git a/pkg/querier/engine/config.go b/pkg/querier/engine/config.go index ebb80a0fd6e..f0c75fa8cc3 100644 --- a/pkg/querier/engine/config.go +++ b/pkg/querier/engine/config.go @@ -9,6 +9,7 @@ import ( "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql" "github.com/grafana/mimir/pkg/streamingpromql" //lint:ignore faillint streamingpromql is fine @@ -66,6 +67,7 @@ func NewPromQLEngineOptions(cfg Config, activityTracker *activitytracker.Activit NoStepSubqueryIntervalFn: func(int64) int64 { return cfg.DefaultEvaluationInterval.Milliseconds() }, + ValidationScheme: validation.LegacyNamingScheme, } cfg.MimirQueryEngine.CommonOpts = commonOpts diff --git a/pkg/querier/stats_renderer_test.go b/pkg/querier/stats_renderer_test.go index 79caa08e487..157475c63a8 100644 --- a/pkg/querier/stats_renderer_test.go +++ b/pkg/querier/stats_renderer_test.go @@ -13,7 +13,6 @@ import ( "github.com/grafana/dskit/user" "github.com/grafana/regexp" "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/common/model" "github.com/prometheus/common/promslog" "github.com/prometheus/common/route" "github.com/prometheus/prometheus/config" @@ -26,12 +25,6 @@ import ( mimir_stats "github.com/grafana/mimir/pkg/querier/stats" ) -func init() { - // Mimir doesn't support Prometheus' UTF-8 metric/label name scheme yet. - // nolint:staticcheck - model.NameValidationScheme = model.LegacyValidation -} - func TestStatsRenderer(t *testing.T) { testCases := map[string]struct { diff --git a/pkg/ruler/manager.go b/pkg/ruler/manager.go index d78a0a77b48..f849a585f70 100644 --- a/pkg/ruler/manager.go +++ b/pkg/ruler/manager.go @@ -21,6 +21,7 @@ import ( "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/rulefmt" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/notifier" promRules "github.com/prometheus/prometheus/rules" "go.opentelemetry.io/otel" @@ -440,7 +441,7 @@ func (r *DefaultMultiTenantManager) ValidateRuleGroup(g rulefmt.RuleGroup, node } for i, r := range g.Rules { - for _, err := range r.Validate(node.Rules[i]) { + for _, err := range r.Validate(node.Rules[i], validation.LegacyNamingScheme) { var ruleName string if r.Alert != "" { ruleName = r.Alert diff --git a/pkg/streamingpromql/config.go b/pkg/streamingpromql/config.go index aeffda5ebf5..98c77e013c6 100644 --- a/pkg/streamingpromql/config.go +++ b/pkg/streamingpromql/config.go @@ -7,6 +7,7 @@ import ( "math" "time" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql" ) @@ -41,6 +42,7 @@ func NewTestEngineOpts() EngineOpts { EnableAtModifier: true, EnableNegativeOffset: true, NoStepSubqueryIntervalFn: func(int64) int64 { return time.Minute.Milliseconds() }, + ValidationScheme: validation.LegacyNamingScheme, }, Pedantic: true, diff --git a/pkg/streamingpromql/operators/aggregations/count_values.go b/pkg/streamingpromql/operators/aggregations/count_values.go index 0212b355d03..95c6aaef243 100644 --- a/pkg/streamingpromql/operators/aggregations/count_values.go +++ b/pkg/streamingpromql/operators/aggregations/count_values.go @@ -12,13 +12,13 @@ import ( "strconv" "sync" - "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser/posrange" "github.com/grafana/mimir/pkg/streamingpromql/types" "github.com/grafana/mimir/pkg/util/limiter" + "github.com/grafana/mimir/pkg/util/validation" ) type CountValues struct { @@ -152,7 +152,7 @@ func (c *CountValues) SeriesMetadata(ctx context.Context) ([]types.SeriesMetadat func (c *CountValues) loadLabelName() error { c.resolvedLabelName = c.LabelName.GetValue() - if !model.LabelName(c.resolvedLabelName).IsValid() { + if !validation.IsValidLabelName(c.resolvedLabelName) { return fmt.Errorf("invalid label name %q", c.resolvedLabelName) } diff --git a/pkg/streamingpromql/operators/functions/label.go b/pkg/streamingpromql/operators/functions/label.go index ad963c5c006..3d81d930910 100644 --- a/pkg/streamingpromql/operators/functions/label.go +++ b/pkg/streamingpromql/operators/functions/label.go @@ -11,25 +11,25 @@ import ( "strings" "github.com/grafana/regexp" - "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" "github.com/grafana/mimir/pkg/streamingpromql/types" "github.com/grafana/mimir/pkg/util/limiter" + "github.com/grafana/mimir/pkg/util/validation" ) func LabelJoinFactory(dstLabelOp, separatorOp types.StringOperator, srcLabelOps []types.StringOperator) SeriesMetadataFunction { return func(seriesMetadata []types.SeriesMetadata, _ *limiter.MemoryConsumptionTracker) ([]types.SeriesMetadata, error) { dst := dstLabelOp.GetValue() - if !model.LabelName(dst).IsValid() { + if !validation.IsValidLabelName(dst) { return nil, fmt.Errorf("invalid destination label name in label_join(): %s", dst) } separator := separatorOp.GetValue() srcLabels := make([]string, len(srcLabelOps)) for i, op := range srcLabelOps { src := op.GetValue() - if !model.LabelName(src).IsValid() { - return nil, fmt.Errorf("invalid source label name in label_join(): %s", dst) + if !validation.IsValidLabelName(src) { + return nil, fmt.Errorf("invalid source label name in label_join(): %s", src) } srcLabels[i] = src } @@ -66,7 +66,7 @@ func LabelReplaceFactory(dstLabelOp, replacementOp, srcLabelOp, regexOp types.St return nil, fmt.Errorf("invalid regular expression in label_replace(): %s", regexStr) } dst := dstLabelOp.GetValue() - if !model.LabelName(dst).IsValid() { + if !validation.IsValidLabelName(dst) { return nil, fmt.Errorf("invalid destination label name in label_replace(): %s", dst) } repl := replacementOp.GetValue() diff --git a/pkg/streamingpromql/planning.go b/pkg/streamingpromql/planning.go index 490adaeca01..046a55ae2a8 100644 --- a/pkg/streamingpromql/planning.go +++ b/pkg/streamingpromql/planning.go @@ -16,6 +16,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/storage" @@ -108,7 +109,7 @@ func (p *QueryPlanner) NewQueryPlan(ctx context.Context, qs string, timeRange ty defer p.activeQueryTracker.Delete(queryID) - expr, err := p.runASTStage("Parsing", observer, func() (parser.Expr, error) { return parser.ParseExpr(qs) }) + expr, err := p.runASTStage("Parsing", observer, func() (parser.Expr, error) { return ParseExpr(qs) }) if err != nil { return nil, err } @@ -704,3 +705,12 @@ func parseDuration(s string) (time.Duration, error) { } return 0, fmt.Errorf("cannot parse %q to a valid duration", s) } + +// ParseExpr is like parser.ParseExpr but uses legacy naming scheme. +func ParseExpr(expr string) (parser.Expr, error) { + p := parser.NewParser( + expr, + parser.WithValidationScheme(validation.LegacyNamingScheme), + ) + return p.ParseExpr() +} diff --git a/pkg/streamingpromql/testdata/ours-only/aggregators.test b/pkg/streamingpromql/testdata/ours-only/aggregators.test new file mode 100644 index 00000000000..e519557ad4d --- /dev/null +++ b/pkg/streamingpromql/testdata/ours-only/aggregators.test @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: AGPL-3.0-only + +# count_values() tests +load 6m + series{idx="0", case="Inf"} Inf Inf Inf + series{idx="0", case="-Inf"} -Inf -Inf _ + +# Prometheus uses UTF-8 validation as of v0.62.0. +# Mimir only supports classic/legacy label names. +eval range from 0 to 12m step 6m count_values("(value)", series) + expect fail msg: invalid label name "(value)" \ No newline at end of file diff --git a/pkg/streamingpromql/testdata/ours-only/functions.test b/pkg/streamingpromql/testdata/ours-only/functions.test index dcadd2059ed..7ee678012da 100644 --- a/pkg/streamingpromql/testdata/ours-only/functions.test +++ b/pkg/streamingpromql/testdata/ours-only/functions.test @@ -1,3 +1,5 @@ +# SPDX-License-Identifier: AGPL-3.0-only + load 6m series{label="a", idx="1"} 2 _ series{label="a", idx="2"} _ 4 @@ -5,3 +7,30 @@ load 6m # Currently prometheus does not merge series: https://github.com/prometheus/prometheus/issues/15114 eval range from 0 to 6m step 6m label_replace(series, "idx", "replaced", "idx", ".*") series{label="a", idx="replaced"} 2 4 + +clear + +# label_join() tests +load 5m + series{label="a", opt="1", this="that"} 1.1 5.0 + +# Prometheus uses UTF-8 validation as of v0.62.0. +# Mimir only supports classic/legacy (source) label names. +eval instant at 0m label_join(series, "export", ",", "(opt)", "this") + expect fail msg: invalid source label name in label_join(): (opt) + +# Prometheus uses UTF-8 validation as of v0.62.0. +# Mimir only supports classic/legacy (destination) label names. +eval instant at 0m label_join(series, "(export)", "-", "label", "opt") + expect fail msg: invalid destination label name in label_join(): (export) + +clear + +# label_replace() tests +load 5m + series{label="a"} 1.1 + +# Prometheus uses UTF-8 validation as of v0.62.0. +# Mimir only supports classic/legacy (source) label names. +eval instant at 0m label_replace(series, "(export)", "$1", "label", "(.*)") + expect fail msg: invalid destination label name in label_replace(): (export) \ No newline at end of file diff --git a/pkg/streamingpromql/testdata/ours/functions.test b/pkg/streamingpromql/testdata/ours/functions.test index e7fd6011460..96aaa615eb8 100644 --- a/pkg/streamingpromql/testdata/ours/functions.test +++ b/pkg/streamingpromql/testdata/ours/functions.test @@ -290,18 +290,6 @@ eval instant at 0m floor(label_join(series, "__name__", "", "label")) {label="b", opt="2", this="there"} 2 {label="c", opt="3"} NaN -# Passes due to UTF-8 validation of source labels -eval range from 0 to 10m step 5m label_join(series, "export", ",", "(opt)", "this") - series{export=",", label="c", opt="3"} NaN - series{export=",that", label="a", opt="1", this="that"} 1.1 5 - series{export=",there", label="b", opt="2", this="there"} 2.9 {{sum:4 count:23 buckets:[1 2 4]}} - -# Passes due to UTF-8 validation of dest labels -eval range from 0 to 10m step 5m label_join(series, "(export)", "-", "label", "opt") - series{"(export)"="a-1", label="a", opt="1", this="that"} 1.1 5.0 - series{"(export)"="b-2", label="b", opt="2", this="there"} 2.9 {{sum:4 count:23 buckets:[1 2 4]}} - series{"(export)"="c-3", label="c", opt="3"} NaN _ - clear # The following tests pass prometheus' engine when promql-delayed-name-removal is enabled. diff --git a/pkg/util/validation/labels.go b/pkg/util/validation/labels.go new file mode 100644 index 00000000000..0293caef751 --- /dev/null +++ b/pkg/util/validation/labels.go @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package validation + +import ( + "github.com/prometheus/common/model" +) + +// IsValidLabelName returns true iff name matches prometheus +// "LegacyValidation" name validation scheme. +func IsValidLabelName(name string) bool { + return model.LabelName(name).IsValidLegacy() +} + +// IsValidMetricName returns true iff the name matches prometheus +// "LegacyValidation" name validation scheme. +func IsValidMetricName(name string) bool { + return model.IsValidLegacyMetricName(name) +} diff --git a/pkg/util/validation/labels_test.go b/pkg/util/validation/labels_test.go new file mode 100644 index 00000000000..6190ac979c2 --- /dev/null +++ b/pkg/util/validation/labels_test.go @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidLabelName(t *testing.T) { + testCases := []struct { + name string + input string + want bool + }{ + { + name: "invalid empty string", + input: "", + want: false, + }, + { + name: "invalid character", + input: "invalid.character", + want: false, + }, + { + name: "invalid UTF-8", + input: "2H₂ + O₂ ⇌ 2H₂O", + want: false, + }, + { + name: "valid label name", + input: "label_name", + want: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, IsValidLabelName(tc.input)) + }) + } +} + +func TestIsValidMetricName(t *testing.T) { + testCases := []struct { + name string + input string + want bool + }{ + { + name: "invalid empty string", + input: "", + want: false, + }, + { + name: "invalid start with number", + input: "123test", + want: false, + }, + { + name: "invalid UTF-8", + input: "2H₂ + O₂ ⇌ 2H₂O", + want: false, + }, + { + name: "valid underscore", + input: "metric_name", + want: true, + }, + { + name: "valid colon", + input: "metric:name", + want: true, + }, + { + name: "valid numbers", + input: "metric_name_123", + want: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, IsValidMetricName(tc.input)) + }) + } +} diff --git a/pkg/util/validation/limits.go b/pkg/util/validation/limits.go index f58394945e4..05011c87182 100644 --- a/pkg/util/validation/limits.go +++ b/pkg/util/validation/limits.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/dskit/flagext" "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/relabel" + "github.com/prometheus/prometheus/model/validation" "go.uber.org/atomic" "golang.org/x/time/rate" "gopkg.in/yaml.v3" @@ -529,6 +530,10 @@ func (l *Limits) validate() error { if cfg == nil { return errors.New("invalid metric_relabel_configs") } + cfg.MetricNameValidationScheme = validation.LegacyNamingScheme + if err := cfg.Validate(validation.LegacyNamingScheme); err != nil { + return err + } } if l.MaxEstimatedChunksPerQueryMultiplier < 1 && l.MaxEstimatedChunksPerQueryMultiplier != 0 { diff --git a/vendor/github.com/prometheus/prometheus/config/config.go b/vendor/github.com/prometheus/prometheus/config/config.go index 06dae61c08c..a8bd5b0e9ae 100644 --- a/vendor/github.com/prometheus/prometheus/config/config.go +++ b/vendor/github.com/prometheus/prometheus/config/config.go @@ -37,6 +37,7 @@ import ( "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/relabel" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/storage/remote/azuread" "github.com/prometheus/prometheus/storage/remote/googleiam" ) @@ -68,11 +69,6 @@ var ( } ) -const ( - LegacyValidationConfig = "legacy" - UTF8ValidationConfig = "utf8" -) - // Load parses the YAML input s into a Config. func Load(s string, logger *slog.Logger) (*Config, error) { cfg := &Config{} @@ -112,7 +108,7 @@ func Load(s string, logger *slog.Logger) (*Config, error) { case UnderscoreEscapingWithSuffixes: case "": case NoTranslation, NoUTF8EscapingWithSuffixes: - if cfg.GlobalConfig.MetricNameValidationScheme == LegacyValidationConfig { + if cfg.GlobalConfig.MetricNameValidationScheme == validation.LegacyNamingScheme { return nil, fmt.Errorf("OTLP translation strategy %q is not allowed when UTF8 is disabled", cfg.OTLPConfig.TranslationStrategy) } default: @@ -417,6 +413,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if rwcfg == nil { return errors.New("empty or null remote write config section") } + if err := rwcfg.Validate(c.GlobalConfig); err != nil { + return err + } // Skip empty names, we fill their name with their config hash in remote write code. if _, ok := rwNames[rwcfg.Name]; ok && rwcfg.Name != "" { return fmt.Errorf("found multiple remote write configs with job name %q", rwcfg.Name) @@ -434,6 +433,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } rrNames[rrcfg.Name] = struct{}{} } + if err := c.AlertingConfig.Validate(c.GlobalConfig); err != nil { + return errors.New("invalid alerting config: " + err.Error()) + } return nil } @@ -481,10 +483,10 @@ type GlobalConfig struct { // 0 means no limit. KeepDroppedTargets uint `yaml:"keep_dropped_targets,omitempty"` // Allow UTF8 Metric and Label Names. Can be blank in config files but must - // have a value if a ScrepeConfig is created programmatically. - MetricNameValidationScheme string `yaml:"metric_name_validation_scheme,omitempty"` + // have a value if a GlobalConfig is created programmatically. + MetricNameValidationScheme validation.NamingScheme `yaml:"metric_name_validation_scheme,omitempty"` // Metric name escaping mode to request through content negotiation. Can be - // blank in config files but must have a value if a ScrepeConfig is created + // blank in config files but must have a value if a ScrapeConfig is created // programmatically. MetricNameEscapingScheme string `yaml:"metric_name_escaping_scheme,omitempty"` // Whether to convert all scraped classic histograms into native histograms with custom buckets. @@ -595,15 +597,7 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } - if err := gc.ExternalLabels.Validate(func(l labels.Label) error { - if !model.LabelName(l.Name).IsValid() { - return fmt.Errorf("%q is not a valid label name", l.Name) - } - if !model.LabelValue(l.Value).IsValid() { - return fmt.Errorf("%q is not a valid label value", l.Value) - } - return nil - }); err != nil { + if err := gc.validateExternalLabels(); err != nil { return err } @@ -637,6 +631,19 @@ func (c *GlobalConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +func (c GlobalConfig) validateExternalLabels() error { + namingScheme := c.MetricNameValidationScheme + return c.ExternalLabels.Validate(func(l labels.Label) error { + if !namingScheme.IsValidLabelName(l.Name) { + return fmt.Errorf("%q is not a valid label name", l.Name) + } + if !model.LabelValue(l.Value).IsValid() { + return fmt.Errorf("%q is not a valid label value", l.Value) + } + return nil + }) +} + // isZero returns true iff the global config is the zero value. func (c *GlobalConfig) isZero() bool { return c.ExternalLabels.IsEmpty() && @@ -749,10 +756,10 @@ type ScrapeConfig struct { // 0 means no limit. KeepDroppedTargets uint `yaml:"keep_dropped_targets,omitempty"` // Allow UTF8 Metric and Label Names. Can be blank in config files but must - // have a value if a ScrepeConfig is created programmatically. - MetricNameValidationScheme string `yaml:"metric_name_validation_scheme,omitempty"` + // have a value if a ScrapeConfig is created programmatically. + MetricNameValidationScheme validation.NamingScheme `yaml:"metric_name_validation_scheme,omitempty"` // Metric name escaping mode to request through content negotiation. Can be - // blank in config files but must have a value if a ScrepeConfig is created + // blank in config files but must have a value if a ScrapeConfig is created // programmatically. MetricNameEscapingScheme string `yaml:"metric_name_escaping_scheme,omitempty"` @@ -781,34 +788,6 @@ func (c *ScrapeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := discovery.UnmarshalYAMLWithInlineConfigs(c, unmarshal); err != nil { return err } - if len(c.JobName) == 0 { - return errors.New("job_name is empty") - } - - // The UnmarshalYAML method of HTTPClientConfig is not being called because it's not a pointer. - // We cannot make it a pointer as the parser panics for inlined pointer structs. - // Thus we just do its validation here. - if err := c.HTTPClientConfig.Validate(); err != nil { - return err - } - - // Check for users putting URLs in target groups. - if len(c.RelabelConfigs) == 0 { - if err := checkStaticTargets(c.ServiceDiscoveryConfigs); err != nil { - return err - } - } - - for _, rlcfg := range c.RelabelConfigs { - if rlcfg == nil { - return errors.New("empty or null target relabeling rule in scrape config") - } - } - for _, rlcfg := range c.MetricRelabelConfigs { - if rlcfg == nil { - return errors.New("empty or null metric relabeling rule in scrape config") - } - } return nil } @@ -871,15 +850,20 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { } } - //nolint:staticcheck - if model.NameValidationScheme != model.UTF8Validation { - return errors.New("model.NameValidationScheme must be set to UTF8") + if len(c.JobName) == 0 { + return errors.New("job_name is empty") + } + // The UnmarshalYAML method of HTTPClientConfig is not being called because it's not a pointer. + // We cannot make it a pointer as the parser panics for inlined pointer structs. + // Thus we just do its validation here. + if err := c.HTTPClientConfig.Validate(); err != nil { + return err } switch globalConfig.MetricNameValidationScheme { case "": - globalConfig.MetricNameValidationScheme = UTF8ValidationConfig - case LegacyValidationConfig, UTF8ValidationConfig: + globalConfig.MetricNameValidationScheme = validation.UTF8NamingScheme + case validation.LegacyNamingScheme, validation.UTF8NamingScheme: default: return fmt.Errorf("unknown global name validation method specified, must be either 'legacy' or 'utf8', got %s", globalConfig.MetricNameValidationScheme) } @@ -887,7 +871,7 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { switch c.MetricNameValidationScheme { case "": c.MetricNameValidationScheme = globalConfig.MetricNameValidationScheme - case LegacyValidationConfig, UTF8ValidationConfig: + case validation.LegacyNamingScheme, validation.UTF8NamingScheme: default: return fmt.Errorf("unknown scrape config name validation method specified, must be either 'legacy' or 'utf8', got %s", c.MetricNameValidationScheme) } @@ -895,7 +879,7 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { // Escaping scheme is based on the validation scheme if left blank. switch globalConfig.MetricNameEscapingScheme { case "": - if globalConfig.MetricNameValidationScheme == LegacyValidationConfig { + if globalConfig.MetricNameValidationScheme == validation.LegacyNamingScheme { globalConfig.MetricNameEscapingScheme = model.EscapeUnderscores } else { globalConfig.MetricNameEscapingScheme = model.AllowUTF8 @@ -911,7 +895,7 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { switch c.MetricNameEscapingScheme { case model.AllowUTF8: - if c.MetricNameValidationScheme != UTF8ValidationConfig { + if c.MetricNameValidationScheme != validation.UTF8NamingScheme { return errors.New("utf8 metric names requested but validation scheme is not set to UTF8") } case model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: @@ -929,6 +913,33 @@ func (c *ScrapeConfig) Validate(globalConfig GlobalConfig) error { c.AlwaysScrapeClassicHistograms = &global } + // Validate relabel configs + namingScheme := c.MetricNameValidationScheme + + // Check for users putting URLs in target groups. + if len(c.RelabelConfigs) == 0 { + if err := checkStaticTargets(c.ServiceDiscoveryConfigs); err != nil { + return err + } + } + + for _, rlcfg := range c.RelabelConfigs { + if rlcfg == nil { + return errors.New("empty or null target relabeling rule in scrape config") + } + if err := rlcfg.Validate(namingScheme); err != nil { + return errors.New("invalid relabel config: " + err.Error()) + } + } + for _, rlcfg := range c.MetricRelabelConfigs { + if rlcfg == nil { + return errors.New("empty or null metric relabeling rule in scrape config") + } + if err := rlcfg.Validate(namingScheme); err != nil { + return errors.New("invalid metric relabel config: " + err.Error()) + } + } + return nil } @@ -937,20 +948,6 @@ func (c *ScrapeConfig) MarshalYAML() (interface{}, error) { return discovery.MarshalYAMLWithInlineConfigs(c) } -// ToValidationScheme returns the validation scheme for the given string config value. -func ToValidationScheme(s string) (validationScheme model.ValidationScheme, err error) { - switch s { - case UTF8ValidationConfig: - validationScheme = model.UTF8Validation - case LegacyValidationConfig: - validationScheme = model.LegacyValidation - default: - return model.UTF8Validation, fmt.Errorf("invalid metric name validation scheme, %s", s) - } - - return validationScheme, nil -} - // ConvertClassicHistogramsToNHCBEnabled returns whether to convert classic histograms to NHCB. func (c *ScrapeConfig) ConvertClassicHistogramsToNHCBEnabled() bool { return c.ConvertClassicHistogramsToNHCB != nil && *c.ConvertClassicHistogramsToNHCB @@ -1092,11 +1089,25 @@ func (c *AlertingConfig) UnmarshalYAML(unmarshal func(interface{}) error) error if err := unmarshal((*plain)(c)); err != nil { return err } + return nil +} +func (c *AlertingConfig) Validate(globalConfig GlobalConfig) error { + for _, amcfg := range c.AlertmanagerConfigs { + if amcfg == nil { + return errors.New("empty or null alert manager config") + } + if err := amcfg.Validate(globalConfig); err != nil { + return err + } + } for _, rlcfg := range c.AlertRelabelConfigs { if rlcfg == nil { return errors.New("empty or null alert relabeling rule") } + if err := rlcfg.Validate(globalConfig.MetricNameValidationScheme); err != nil { + return err + } } return nil } @@ -1169,6 +1180,10 @@ type AlertmanagerConfig struct { RelabelConfigs []*relabel.Config `yaml:"relabel_configs,omitempty"` // Relabel alerts before sending to the specific alertmanager. AlertRelabelConfigs []*relabel.Config `yaml:"alert_relabel_configs,omitempty"` + + // Allow UTF8 Metric and Label Names. Can be blank in config files but must + // have a value if a AlertmanagerConfig is created programmatically. + MetricNameValidationScheme validation.NamingScheme `yaml:"metric_name_validation_scheme,omitempty"` } // SetDirectory joins any relative file paths with dir. @@ -1183,7 +1198,10 @@ func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) er if err := discovery.UnmarshalYAMLWithInlineConfigs(c, unmarshal); err != nil { return err } + return nil +} +func (c *AlertmanagerConfig) Validate(globalConfig GlobalConfig) error { // The UnmarshalYAML method of HTTPClientConfig is not being called because it's not a pointer. // We cannot make it a pointer as the parser panics for inlined pointer structs. // Thus we just do its validation here. @@ -1205,16 +1223,28 @@ func (c *AlertmanagerConfig) UnmarshalYAML(unmarshal func(interface{}) error) er } } + c.MetricNameValidationScheme = c.MetricNameValidationScheme.WithDefault(globalConfig.MetricNameValidationScheme) + if err := c.MetricNameValidationScheme.Validate(); err != nil { + return err + } + + namingScheme := c.MetricNameValidationScheme for _, rlcfg := range c.RelabelConfigs { if rlcfg == nil { return errors.New("empty or null Alertmanager target relabeling rule") } + if err := rlcfg.Validate(namingScheme); err != nil { + return errors.New("invalid relabel config: " + err.Error()) + } } for _, rlcfg := range c.AlertRelabelConfigs { if rlcfg == nil { return errors.New("empty or null Alertmanager alert relabeling rule") } + if err := rlcfg.Validate(namingScheme); err != nil { + return errors.New("invalid alert relabel config: " + err.Error()) + } } return nil @@ -1317,6 +1347,10 @@ type RemoteWriteConfig struct { SigV4Config *sigv4.SigV4Config `yaml:"sigv4,omitempty"` AzureADConfig *azuread.AzureADConfig `yaml:"azuread,omitempty"` GoogleIAMConfig *googleiam.Config `yaml:"google_iam,omitempty"` + + // Allow UTF8 Metric and Label Names. Can be blank in config files but must + // have a value if a RemoteWriteConfig is created programmatically. + MetricNameValidationScheme validation.NamingScheme `yaml:"metric_name_validation_scheme,omitempty"` } // SetDirectory joins any relative file paths with dir. @@ -1331,14 +1365,13 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err if err := unmarshal((*plain)(c)); err != nil { return err } + return nil +} + +func (c *RemoteWriteConfig) Validate(globalConfig GlobalConfig) error { if c.URL == nil { return errors.New("url for remote_write is empty") } - for _, rlcfg := range c.WriteRelabelConfigs { - if rlcfg == nil { - return errors.New("empty or null relabeling rule in remote write config") - } - } if err := validateHeaders(c.Headers); err != nil { return err } @@ -1349,12 +1382,28 @@ func (c *RemoteWriteConfig) UnmarshalYAML(unmarshal func(interface{}) error) err // The UnmarshalYAML method of HTTPClientConfig is not being called because it's not a pointer. // We cannot make it a pointer as the parser panics for inlined pointer structs. - // Thus we just do its validation here. + // Thus, we just do its validation here. if err := c.HTTPClientConfig.Validate(); err != nil { return err } - return validateAuthConfigs(c) + if err := validateAuthConfigs(c); err != nil { + return err + } + + namingScheme := c.MetricNameValidationScheme.WithDefault(globalConfig.MetricNameValidationScheme) + if err := namingScheme.Validate(); err != nil { + return errors.New("invalid metric name validation scheme: " + err.Error()) + } + for _, rlcfg := range c.WriteRelabelConfigs { + if rlcfg == nil { + return errors.New("empty or null relabeling rule in remote write config") + } + if err := rlcfg.Validate(namingScheme); err != nil { + return errors.New("invalid relabel config: " + err.Error()) + } + } + return nil } // validateAuthConfigs validates that at most one of basic_auth, authorization, oauth2, sigv4, azuread or google_iam must be configured. @@ -1472,6 +1521,10 @@ type RemoteReadConfig struct { // Whether to use the external labels as selectors for the remote read endpoint. FilterExternalLabels bool `yaml:"filter_external_labels,omitempty"` + + // Allow UTF8 Metric and Label Names. Can be blank in config files but must + // have a value if a RemoteReadConfig is created programmatically. + MetricNameValidationScheme validation.NamingScheme `yaml:"metric_name_validation_scheme,omitempty"` } // SetDirectory joins any relative file paths with dir. diff --git a/vendor/github.com/prometheus/prometheus/model/labels/labels_common.go b/vendor/github.com/prometheus/prometheus/model/labels/labels_common.go index 5f46d6c35f4..e842acf5e7f 100644 --- a/vendor/github.com/prometheus/prometheus/model/labels/labels_common.go +++ b/vendor/github.com/prometheus/prometheus/model/labels/labels_common.go @@ -21,6 +21,8 @@ import ( "unsafe" "github.com/prometheus/common/model" + + "github.com/prometheus/prometheus/model/validation" ) const ( @@ -101,23 +103,16 @@ func (ls *Labels) UnmarshalYAML(unmarshal func(interface{}) error) error { } // IsValid checks if the metric name or label names are valid. -func (ls Labels) IsValid(validationScheme model.ValidationScheme) bool { +// +//nolint:forbidigo +func (ls Labels) IsValid(validationScheme validation.NamingScheme) bool { err := ls.Validate(func(l Label) error { 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 diff --git a/vendor/github.com/prometheus/prometheus/model/relabel/relabel.go b/vendor/github.com/prometheus/prometheus/model/relabel/relabel.go index 70daef426f5..14fbb277280 100644 --- a/vendor/github.com/prometheus/prometheus/model/relabel/relabel.go +++ b/vendor/github.com/prometheus/prometheus/model/relabel/relabel.go @@ -26,6 +26,7 @@ import ( "github.com/prometheus/common/model" "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/validation" ) var ( @@ -100,6 +101,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"` + // MetricNameValidationScheme to use when validating labels. + MetricNameValidationScheme validation.NamingScheme `yaml:"metric_name_validation_scheme,omitempty" json:"metricNameValidationScheme,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. @@ -112,10 +115,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(defaultNamingScheme validation.NamingScheme) error { if c.Action == "" { return errors.New("relabel action cannot be empty") } @@ -125,29 +128,30 @@ 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() { - return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) + + c.MetricNameValidationScheme = c.MetricNameValidationScheme.WithDefault(defaultNamingScheme) + if err := c.MetricNameValidationScheme.Validate(); err != nil { + return err } - isValidLabelNameWithRegexVarFn := func(value string) bool { - // 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() + namingScheme := c.MetricNameValidationScheme + if c.Action == Replace && !varInRegexTemplate(c.TargetLabel) && !namingScheme.IsValidLabelName(c.TargetLabel) { + return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) } - if c.Action == Replace && varInRegexTemplate(c.TargetLabel) && !isValidLabelNameWithRegexVarFn(c.TargetLabel) { + + if c.Action == Replace && varInRegexTemplate(c.TargetLabel) && !namingScheme.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) && !model.LabelName(c.TargetLabel).IsValid() { + if (c.Action == Lowercase || c.Action == Uppercase || c.Action == KeepEqual || c.Action == DropEqual) && !namingScheme.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 { return fmt.Errorf("'replacement' can not be set for %s action", c.Action) } - if c.Action == LabelMap && !isValidLabelNameWithRegexVarFn(c.Replacement) { + if c.Action == LabelMap && !namingScheme.IsValidLabelName(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 && !namingScheme.IsValidLabelName(c.TargetLabel) { return fmt.Errorf("%q is invalid 'target_label' for %s action", c.TargetLabel, c.Action) } @@ -318,16 +322,17 @@ 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)) + namingScheme := cfg.MetricNameValidationScheme + if !namingScheme.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: diff --git a/vendor/github.com/prometheus/prometheus/model/rulefmt/rulefmt.go b/vendor/github.com/prometheus/prometheus/model/rulefmt/rulefmt.go index 96b70cc66b0..9bb185caf32 100644 --- a/vendor/github.com/prometheus/prometheus/model/rulefmt/rulefmt.go +++ b/vendor/github.com/prometheus/prometheus/model/rulefmt/rulefmt.go @@ -27,6 +27,7 @@ import ( "gopkg.in/yaml.v3" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/template" @@ -96,7 +97,7 @@ 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, namingScheme validation.NamingScheme) (errs []error) { set := map[string]struct{}{} for j, g := range g.Groups { @@ -112,7 +113,7 @@ func (g *RuleGroups) Validate(node ruleGroups) (errs []error) { } for k, v := range g.Labels { - if !model.LabelName(k).IsValid() || k == model.MetricNameLabel { + if !namingScheme.IsValidLabelName(k) || k == model.MetricNameLabel { errs = append( errs, fmt.Errorf("invalid label name: %s", k), ) @@ -128,7 +129,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], namingScheme) { var ruleName string if r.Alert != "" { ruleName = r.Alert @@ -198,7 +199,7 @@ type RuleNode struct { } // Validate the rule and return a list of encountered errors. -func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { +func (r *Rule) Validate(node RuleNode, namingScheme validation.NamingScheme) (nodes []WrappedError) { if r.Record != "" && r.Alert != "" { nodes = append(nodes, WrappedError{ err: errors.New("only one of 'record' and 'alert' must be set"), @@ -244,7 +245,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { node: &node.Record, }) } - if !model.IsValidMetricName(model.LabelValue(r.Record)) { + if !namingScheme.IsValidMetricName(r.Record) { nodes = append(nodes, WrappedError{ err: fmt.Errorf("invalid recording rule name: %s", r.Record), node: &node.Record, @@ -261,7 +262,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { } for k, v := range r.Labels { - if !model.LabelName(k).IsValid() || k == model.MetricNameLabel { + if !namingScheme.IsValidLabelName(k) || k == model.MetricNameLabel { nodes = append(nodes, WrappedError{ err: fmt.Errorf("invalid label name: %s", k), }) @@ -275,7 +276,7 @@ func (r *Rule) Validate(node RuleNode) (nodes []WrappedError) { } for k := range r.Annotations { - if !model.LabelName(k).IsValid() { + if !namingScheme.IsValidLabelName(k) { nodes = append(nodes, WrappedError{ err: fmt.Errorf("invalid annotation name: %s", k), }) @@ -338,8 +339,27 @@ func testTemplateParsing(rl *Rule) (errs []error) { return errs } +type parseArgs struct { + namingScheme validation.NamingScheme +} + +type ParseOption func(*parseArgs) + +func WithNamingScheme(scheme validation.NamingScheme) ParseOption { + return func(args *parseArgs) { + args.namingScheme = scheme + } +} + // Parse parses and validates a set of rules. -func Parse(content []byte, ignoreUnknownFields bool) (*RuleGroups, []error) { +func Parse(content []byte, ignoreUnknownFields bool, opts ...ParseOption) (*RuleGroups, []error) { + args := &parseArgs{ + namingScheme: validation.UTF8NamingScheme, + } + for _, opt := range opts { + opt(args) + } + var ( groups RuleGroups node ruleGroups @@ -364,16 +384,16 @@ func Parse(content []byte, ignoreUnknownFields bool) (*RuleGroups, []error) { return nil, errs } - return &groups, groups.Validate(node) + return &groups, groups.Validate(node, args.namingScheme) } // ParseFile reads and parses rules from a file. -func ParseFile(file string, ignoreUnknownFields bool) (*RuleGroups, []error) { +func ParseFile(file string, ignoreUnknownFields bool, opts ...ParseOption) (*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, opts...) for i := range errs { errs[i] = fmt.Errorf("%s: %w", file, errs[i]) } diff --git a/vendor/github.com/prometheus/prometheus/model/textparse/protobufparse.go b/vendor/github.com/prometheus/prometheus/model/textparse/protobufparse.go index 2ca6c03af71..f4d9257f281 100644 --- a/vendor/github.com/prometheus/prometheus/model/textparse/protobufparse.go +++ b/vendor/github.com/prometheus/prometheus/model/textparse/protobufparse.go @@ -30,6 +30,7 @@ import ( "github.com/prometheus/prometheus/model/exemplar" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/validation" dto "github.com/prometheus/prometheus/prompb/io/prometheus/client" "github.com/prometheus/prometheus/schema" ) @@ -80,11 +81,21 @@ type ProtobufParser struct { // native histogram. parseClassicHistograms bool enableTypeAndUnitLabels bool + + namingScheme validation.NamingScheme +} + +type ProtobufParserOption func(p *ProtobufParser) + +func WithNamingScheme(scheme validation.NamingScheme) ProtobufParserOption { + return func(p *ProtobufParser) { + p.namingScheme = scheme + } } // NewProtobufParser returns a parser for the payload in the byte slice. -func NewProtobufParser(b []byte, parseClassicHistograms, enableTypeAndUnitLabels bool, st *labels.SymbolTable) Parser { - return &ProtobufParser{ +func NewProtobufParser(b []byte, parseClassicHistograms, enableTypeAndUnitLabels bool, st *labels.SymbolTable, opts ...ProtobufParserOption) Parser { + pp := &ProtobufParser{ dec: dto.NewMetricStreamingDecoder(b), entryBytes: &bytes.Buffer{}, builder: labels.NewScratchBuilderWithSymbolTable(st, 16), // TODO(bwplotka): Try base builder. @@ -92,7 +103,12 @@ func NewProtobufParser(b []byte, parseClassicHistograms, enableTypeAndUnitLabels state: EntryInvalid, parseClassicHistograms: parseClassicHistograms, enableTypeAndUnitLabels: enableTypeAndUnitLabels, + namingScheme: validation.UTF8NamingScheme, + } + for _, opt := range opts { + opt(pp) } + return pp } // Series returns the bytes of a series with a simple float64 as a @@ -428,7 +444,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 !p.namingScheme.IsValidMetricName(name) { return EntryInvalid, fmt.Errorf("invalid metric name: %s", name) } if help := p.dec.GetHelp(); !utf8.ValidString(help) { diff --git a/vendor/github.com/prometheus/prometheus/model/validation/name_validation.go b/vendor/github.com/prometheus/prometheus/model/validation/name_validation.go new file mode 100644 index 00000000000..aecf89892dc --- /dev/null +++ b/vendor/github.com/prometheus/prometheus/model/validation/name_validation.go @@ -0,0 +1,58 @@ +package validation + +import ( + "fmt" + "unicode/utf8" + + "github.com/prometheus/common/model" +) + +// NamingScheme that is used for validation label and metric names. +type NamingScheme string + +const ( + // LegacyNamingScheme validates label and metric names with the legacy naming convention. + LegacyNamingScheme NamingScheme = "legacy" + // UTF8NamingScheme validates label and metric names according to UTF8 naming convention. + UTF8NamingScheme NamingScheme = "utf8" +) + +// Validate the NamingScheme is one of LegacyNamingScheme, UTF8NamingScheme, or unset (""). +// If s is unset, NamingScheme defaults to UTF8NamingScheme. +func (s NamingScheme) Validate() error { + switch s { + case "", LegacyNamingScheme, UTF8NamingScheme: + return nil + } + return fmt.Errorf("invalid name validation scheme %q", s) +} + +// WithDefault returns s if it is set (s != ""), defaultScheme otherwise. +func (s NamingScheme) WithDefault(defaultScheme NamingScheme) NamingScheme { + if s == "" { + return defaultScheme + } + return s +} + +// IsValidLabelName ensures name adheres to the NamingScheme. +func (s NamingScheme) IsValidLabelName(name string) bool { + if s == LegacyNamingScheme { + return model.LabelName(name).IsValidLegacy() + } + if len(name) == 0 { + return false + } + return utf8.ValidString(name) +} + +// IsValidMetricName ensures name adheres to the NamingScheme. +func (s NamingScheme) IsValidMetricName(name string) bool { + if s == LegacyNamingScheme { + return model.IsValidLegacyMetricName(name) + } + if len(name) == 0 { + return false + } + return utf8.ValidString(name) +} diff --git a/vendor/github.com/prometheus/prometheus/prompb/io/prometheus/client/decoder.go b/vendor/github.com/prometheus/prometheus/prompb/io/prometheus/client/decoder.go index d4fb4204cae..98383968fbe 100644 --- a/vendor/github.com/prometheus/prometheus/prompb/io/prometheus/client/decoder.go +++ b/vendor/github.com/prometheus/prometheus/prompb/io/prometheus/client/decoder.go @@ -22,7 +22,8 @@ import ( "unsafe" proto "github.com/gogo/protobuf/proto" - "github.com/prometheus/common/model" + + "github.com/prometheus/prometheus/model/validation" ) type MetricStreamingDecoder struct { @@ -40,6 +41,16 @@ type MetricStreamingDecoder struct { mData []byte labels []pos + + namingScheme validation.NamingScheme +} + +type Option func(*MetricStreamingDecoder) + +func WithNamingScheme(scheme validation.NamingScheme) Option { + return func(m *MetricStreamingDecoder) { + m.namingScheme = scheme + } } // NewMetricStreamingDecoder returns a Go iterator that unmarshals given protobuf bytes one @@ -51,13 +62,18 @@ type MetricStreamingDecoder struct { // method to use when checking the value. // // TODO(bwplotka): io.Reader approach is possible too, but textparse has access to whole scrape for now. -func NewMetricStreamingDecoder(data []byte) *MetricStreamingDecoder { - return &MetricStreamingDecoder{ +func NewMetricStreamingDecoder(data []byte, opts ...Option) *MetricStreamingDecoder { + msd := &MetricStreamingDecoder{ in: data, MetricFamily: &MetricFamily{}, Metric: &Metric{}, metrics: make([]pos, 0, 100), + namingScheme: validation.UTF8NamingScheme, + } + for _, opt := range opts { + opt(msd) } + return msd } var errInvalidVarint = errors.New("clientpb: invalid varint encountered") @@ -162,7 +178,7 @@ type scratchBuilder interface { // structs tailored for streaming decoding. func (m *MetricStreamingDecoder) Label(b scratchBuilder) error { for _, l := range m.labels { - if err := parseLabel(m.mData[l.start:l.end], b); err != nil { + if err := parseLabel(m.mData[l.start:l.end], b, m.namingScheme.IsValidLabelName); err != nil { return err } } @@ -171,7 +187,7 @@ func (m *MetricStreamingDecoder) Label(b scratchBuilder) error { // parseLabel is essentially LabelPair.Unmarshal but directly adding into scratch builder // and reusing strings. -func parseLabel(dAtA []byte, b scratchBuilder) error { +func parseLabel(dAtA []byte, b scratchBuilder, validate func(string) bool) error { var name, value string l := len(dAtA) iNdEx := 0 @@ -232,7 +248,7 @@ func parseLabel(dAtA []byte, b scratchBuilder) error { return io.ErrUnexpectedEOF } name = yoloString(dAtA[iNdEx:postIndex]) - if !model.LabelName(name).IsValid() { + if !validate(name) { return fmt.Errorf("invalid label name: %s", name) } iNdEx = postIndex diff --git a/vendor/github.com/prometheus/prometheus/promql/engine.go b/vendor/github.com/prometheus/prometheus/promql/engine.go index 752b589fdcf..b92eecc87bf 100644 --- a/vendor/github.com/prometheus/prometheus/promql/engine.go +++ b/vendor/github.com/prometheus/prometheus/promql/engine.go @@ -41,6 +41,7 @@ import ( "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/model/value" "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/promql/parser/posrange" @@ -328,6 +329,9 @@ type EngineOpts struct { EnableDelayedNameRemoval bool // EnableTypeAndUnitLabels will allow PromQL Engine to make decisions based on the type and unit labels. EnableTypeAndUnitLabels bool + + // ValidationScheme to use when validating metric and label names. Defaults to UTF8Validation. + ValidationScheme validation.NamingScheme } // Engine handles the lifetime of queries from beginning to end. @@ -347,6 +351,7 @@ type Engine struct { enablePerStepStats bool enableDelayedNameRemoval bool enableTypeAndUnitLabels bool + nameValidationScheme validation.NamingScheme } // NewEngine returns a new engine. @@ -354,6 +359,9 @@ func NewEngine(opts EngineOpts) *Engine { if opts.Logger == nil { opts.Logger = promslog.NewNopLogger() } + if opts.ValidationScheme == "" { + opts.ValidationScheme = validation.UTF8NamingScheme + } queryResultSummary := prometheus.NewSummaryVec(prometheus.SummaryOpts{ Namespace: namespace, @@ -439,6 +447,7 @@ func NewEngine(opts EngineOpts) *Engine { enablePerStepStats: opts.EnablePerStepStats, enableDelayedNameRemoval: opts.EnableDelayedNameRemoval, enableTypeAndUnitLabels: opts.EnableTypeAndUnitLabels, + nameValidationScheme: opts.ValidationScheme, } } @@ -751,6 +760,7 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval enableDelayedNameRemoval: ng.enableDelayedNameRemoval, enableTypeAndUnitLabels: ng.enableTypeAndUnitLabels, querier: querier, + nameValidationScheme: ng.nameValidationScheme, } query.sampleStats.InitStepTracking(start, start, 1) @@ -809,8 +819,9 @@ func (ng *Engine) execEvalStmt(ctx context.Context, query *query, s *parser.Eval samplesStats: query.sampleStats, noStepSubqueryIntervalFn: ng.noStepSubqueryIntervalFn, enableDelayedNameRemoval: ng.enableDelayedNameRemoval, - enableTypeAndUnitLabels: ng.enableTypeAndUnitLabels, querier: querier, + enableTypeAndUnitLabels: ng.enableTypeAndUnitLabels, + nameValidationScheme: ng.nameValidationScheme, } query.sampleStats.InitStepTracking(evaluator.startTimestamp, evaluator.endTimestamp, evaluator.interval) val, warnings, err := evaluator.Eval(ctxInnerEval, s.Expr) @@ -1085,6 +1096,7 @@ type evaluator struct { enableDelayedNameRemoval bool enableTypeAndUnitLabels bool querier storage.Querier + nameValidationScheme validation.NamingScheme } // errorf causes a panic with the input formatted into an error. @@ -1672,7 +1684,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value, if e.Op == parser.COUNT_VALUES { valueLabel := param.(*parser.StringLiteral) - if !model.LabelName(valueLabel.Val).IsValid() { + if !ev.nameValidationScheme.IsValidLabelName(valueLabel.Val) { ev.errorf("invalid label name %s", valueLabel) } if !e.Without { @@ -2078,6 +2090,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value, enableDelayedNameRemoval: ev.enableDelayedNameRemoval, enableTypeAndUnitLabels: ev.enableTypeAndUnitLabels, querier: ev.querier, + nameValidationScheme: ev.nameValidationScheme, } if e.Step != 0 { @@ -2124,6 +2137,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value, enableDelayedNameRemoval: ev.enableDelayedNameRemoval, enableTypeAndUnitLabels: ev.enableTypeAndUnitLabels, querier: ev.querier, + nameValidationScheme: ev.nameValidationScheme, } res, ws := newEv.eval(ctx, e.Expr) ev.currentSamples = newEv.currentSamples diff --git a/vendor/github.com/prometheus/prometheus/promql/functions.go b/vendor/github.com/prometheus/prometheus/promql/functions.go index 4eb6dfb65ea..0481f899020 100644 --- a/vendor/github.com/prometheus/prometheus/promql/functions.go +++ b/vendor/github.com/prometheus/prometheus/promql/functions.go @@ -1504,7 +1504,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 !ev.nameValidationScheme.IsValidLabelName(dst) { panic(fmt.Errorf("invalid destination label name in label_replace(): %s", dst)) } @@ -1552,12 +1552,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 !ev.nameValidationScheme.IsValidLabelName(src) { panic(fmt.Errorf("invalid source label name in label_join(): %s", src)) } srcLabels[i-3] = src } - if !model.LabelName(dst).IsValid() { + if !ev.nameValidationScheme.IsValidLabelName(dst) { panic(fmt.Errorf("invalid destination label name in label_join(): %s", dst)) } diff --git a/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y b/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y index 7128fc250a3..5c958f96290 100644 --- a/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y +++ b/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y @@ -23,8 +23,6 @@ import ( "github.com/prometheus/prometheus/model/value" "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/promql/parser/posrange" - - "github.com/prometheus/common/model" ) %} @@ -377,14 +375,14 @@ grouping_label_list: grouping_label : maybe_label { - if !model.LabelName($1.Val).IsValid() { + if !yylex.(*parser).nameValidationScheme.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 !yylex.(*parser).nameValidationScheme.IsValidLabelName(unquoted) { yylex.(*parser).addParseErrf($1.PositionRange(),"invalid label name for grouping: %q", unquoted) } $$ = $1 diff --git a/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y.go b/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y.go index 470bf1742a8..a0735fb96b5 100644 --- a/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y.go +++ b/vendor/github.com/prometheus/prometheus/promql/parser/generated_parser.y.go @@ -12,8 +12,6 @@ import ( "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/value" "github.com/prometheus/prometheus/promql/parser/posrange" - - "github.com/prometheus/common/model" ) type yySymType struct { @@ -1292,7 +1290,7 @@ yydefault: case 59: yyDollar = yyS[yypt-1 : yypt+1] { - if !model.LabelName(yyDollar[1].item.Val).IsValid() { + if !yylex.(*parser).nameValidationScheme.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 @@ -1301,7 +1299,7 @@ yydefault: yyDollar = yyS[yypt-1 : yypt+1] { unquoted := yylex.(*parser).unquoteString(yyDollar[1].item.Val) - if !model.LabelName(unquoted).IsValid() { + if !yylex.(*parser).nameValidationScheme.IsValidLabelName(unquoted) { yylex.(*parser).addParseErrf(yyDollar[1].item.PositionRange(), "invalid label name for grouping: %q", unquoted) } yyVAL.item = yyDollar[1].item diff --git a/vendor/github.com/prometheus/prometheus/promql/parser/parse.go b/vendor/github.com/prometheus/prometheus/promql/parser/parse.go index a2a400a7045..130ed04a6d6 100644 --- a/vendor/github.com/prometheus/prometheus/promql/parser/parse.go +++ b/vendor/github.com/prometheus/prometheus/promql/parser/parse.go @@ -29,6 +29,7 @@ import ( "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql/parser/posrange" "github.com/prometheus/prometheus/util/strutil" ) @@ -71,6 +72,8 @@ type parser struct { generatedParserResult interface{} parseErrors ParseErrors + + nameValidationScheme validation.NamingScheme } type Opt func(p *parser) @@ -81,6 +84,13 @@ func WithFunctions(functions map[string]*Function) Opt { } } +// WithValidationScheme controls how labels are validated at parse time. +func WithValidationScheme(scheme validation.NamingScheme) Opt { + return func(p *parser) { + p.nameValidationScheme = scheme + } +} + // NewParser returns a new parser. func NewParser(input string, opts ...Opt) *parser { //nolint:revive // unexported-return p := parserPool.Get().(*parser) @@ -90,6 +100,7 @@ func NewParser(input string, opts ...Opt) *parser { //nolint:revive // unexporte p.parseErrors = nil p.generatedParserResult = nil p.closingParens = make([]posrange.Pos, 0) + p.nameValidationScheme = validation.UTF8NamingScheme // Clear lexer struct before reusing. p.lex = Lexer{ diff --git a/vendor/github.com/prometheus/prometheus/scrape/scrape.go b/vendor/github.com/prometheus/prometheus/scrape/scrape.go index cd6f7719be9..cab5aa9b0cf 100644 --- a/vendor/github.com/prometheus/prometheus/scrape/scrape.go +++ b/vendor/github.com/prometheus/prometheus/scrape/scrape.go @@ -46,6 +46,7 @@ import ( "github.com/prometheus/prometheus/model/relabel" "github.com/prometheus/prometheus/model/textparse" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/model/value" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/util/logging" @@ -103,7 +104,7 @@ type scrapePool struct { scrapeFailureLogger FailureLogger scrapeFailureLoggerMtx sync.RWMutex - validationScheme model.ValidationScheme + validationScheme validation.NamingScheme escapingScheme model.EscapingScheme } @@ -149,8 +150,8 @@ func newScrapePool(cfg *config.ScrapeConfig, app storage.Appendable, offsetSeed return nil, fmt.Errorf("error creating HTTP client: %w", err) } - validationScheme, err := config.ToValidationScheme(cfg.MetricNameValidationScheme) - if err != nil { + validationScheme := cfg.MetricNameValidationScheme + if err := validationScheme.Validate(); err != nil { return nil, fmt.Errorf("invalid metric name validation scheme: %w", err) } var escapingScheme model.EscapingScheme @@ -325,8 +326,8 @@ func (sp *scrapePool) reload(cfg *config.ScrapeConfig) error { sp.config = cfg oldClient := sp.client sp.client = client - validationScheme, err := config.ToValidationScheme(cfg.MetricNameValidationScheme) - if err != nil { + validationScheme := cfg.MetricNameValidationScheme + if err := validationScheme.Validate(); err != nil { return fmt.Errorf("invalid metric name validation scheme: %w", err) } sp.validationScheme = validationScheme @@ -926,7 +927,7 @@ type scrapeLoop struct { timeout time.Duration alwaysScrapeClassicHist bool convertClassicHistToNHCB bool - validationScheme model.ValidationScheme + validationScheme validation.NamingScheme escapingScheme model.EscapingScheme fallbackScrapeProtocol string @@ -1248,7 +1249,7 @@ func newScrapeLoop(ctx context.Context, passMetadataInContext bool, metrics *scrapeMetrics, skipOffsetting bool, - validationScheme model.ValidationScheme, + validationScheme validation.NamingScheme, escapingScheme model.EscapingScheme, fallbackScrapeProtocol string, ) *scrapeLoop { diff --git a/vendor/github.com/prometheus/prometheus/storage/remote/client.go b/vendor/github.com/prometheus/prometheus/storage/remote/client.go index 68891f659e6..47b238a3a11 100644 --- a/vendor/github.com/prometheus/prometheus/storage/remote/client.go +++ b/vendor/github.com/prometheus/prometheus/storage/remote/client.go @@ -38,6 +38,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/prompb" "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/storage/remote/azuread" @@ -121,6 +122,8 @@ type Client struct { writeProtoMsg config.RemoteWriteProtoMsg writeCompression compression.Type // Not exposed by ClientConfig for now. + + namingScheme validation.NamingScheme } // ClientConfig configures a client. @@ -136,6 +139,7 @@ type ClientConfig struct { WriteProtoMsg config.RemoteWriteProtoMsg ChunkedReadLimit uint64 RoundRobinDNS bool + NamingScheme validation.NamingScheme } // ReadClient will request the STREAMED_XOR_CHUNKS method of remote read but can @@ -166,6 +170,7 @@ func NewReadClient(name string, conf *ClientConfig, optFuncs ...config_util.HTTP readQueries: remoteReadQueries.WithLabelValues(name, conf.URL.String()), readQueriesTotal: remoteReadQueriesTotal.MustCurryWith(prometheus.Labels{remoteName: name, endpoint: conf.URL.String()}), readQueriesDuration: remoteReadQueryDuration.MustCurryWith(prometheus.Labels{remoteName: name, endpoint: conf.URL.String()}), + namingScheme: conf.NamingScheme, }, nil } @@ -446,5 +451,5 @@ func (c *Client) handleSampledResponse(req *prompb.ReadRequest, httpResp *http.R // This client does not batch queries so there's always only 1 result. res := resp.Results[0] - return FromQueryResult(sortSeries, res), nil + return FromQueryResult(sortSeries, res, WithNameValidation(c.namingScheme)), nil } diff --git a/vendor/github.com/prometheus/prometheus/storage/remote/codec.go b/vendor/github.com/prometheus/prometheus/storage/remote/codec.go index 80bb8115003..0b6b9fdcaf8 100644 --- a/vendor/github.com/prometheus/prometheus/storage/remote/codec.go +++ b/vendor/github.com/prometheus/prometheus/storage/remote/codec.go @@ -31,6 +31,7 @@ import ( "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/prompb" writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage" @@ -173,12 +174,31 @@ func ToQueryResult(ss storage.SeriesSet, sampleLimit int) (*prompb.QueryResult, return resp, ss.Warnings(), ss.Err() } +type FromQueryResultArgs struct { + nameValidation validation.NamingScheme +} + +type FromQueryResultOption func(*FromQueryResultArgs) + +func WithNameValidation(nameValidation validation.NamingScheme) FromQueryResultOption { + return func(args *FromQueryResultArgs) { + args.nameValidation = nameValidation + } +} + // FromQueryResult unpacks and sorts a QueryResult proto. -func FromQueryResult(sortSeries bool, res *prompb.QueryResult) storage.SeriesSet { +func FromQueryResult(sortSeries bool, res *prompb.QueryResult, opts ...FromQueryResultOption) storage.SeriesSet { + args := &FromQueryResultArgs{ + nameValidation: validation.UTF8NamingScheme, + } + for _, opt := range opts { + opt(args) + } + b := labels.NewScratchBuilder(0) series := make([]storage.Series, 0, len(res.Timeseries)) for _, ts := range res.Timeseries { - if err := validateLabelsAndMetricName(ts.Labels); err != nil { + if err := validateLabelsAndMetricName(ts.Labels, args.nameValidation); err != nil { return errSeriesSet{err: err} } lbls := ts.ToLabels(&b, nil) @@ -756,12 +776,12 @@ func (it *chunkedSeriesIterator) Err() error { // validateLabelsAndMetricName validates the label names/values and metric names returned from remote read, // also making sure that there are no labels with duplicate names. -func validateLabelsAndMetricName(ls []prompb.Label) error { +func validateLabelsAndMetricName(ls []prompb.Label, namingScheme validation.NamingScheme) error { for i, l := range ls { - if l.Name == labels.MetricName && !model.IsValidMetricName(model.LabelValue(l.Value)) { + if l.Name == labels.MetricName && !namingScheme.IsValidMetricName(l.Value) { return fmt.Errorf("invalid metric name: %v", l.Value) } - if !model.LabelName(l.Name).IsValid() { + if !namingScheme.IsValidLabelName(l.Name) { return fmt.Errorf("invalid label name: %v", l.Name) } if !model.LabelValue(l.Value).IsValid() { diff --git a/vendor/github.com/prometheus/prometheus/storage/remote/storage.go b/vendor/github.com/prometheus/prometheus/storage/remote/storage.go index ba6d100bdff..e25cb6debc7 100644 --- a/vendor/github.com/prometheus/prometheus/storage/remote/storage.go +++ b/vendor/github.com/prometheus/prometheus/storage/remote/storage.go @@ -122,6 +122,7 @@ func (s *Storage) ApplyConfig(conf *config.Config) error { ChunkedReadLimit: rrConf.ChunkedReadLimit, HTTPClientConfig: rrConf.HTTPClientConfig, Headers: rrConf.Headers, + NamingScheme: rrConf.MetricNameValidationScheme, }) if err != nil { return err diff --git a/vendor/github.com/prometheus/prometheus/storage/remote/write_handler.go b/vendor/github.com/prometheus/prometheus/storage/remote/write_handler.go index 3ce755eb4dc..15cd5d6f303 100644 --- a/vendor/github.com/prometheus/prometheus/storage/remote/write_handler.go +++ b/vendor/github.com/prometheus/prometheus/storage/remote/write_handler.go @@ -27,7 +27,6 @@ import ( deltatocumulative "github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/common/model" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/pdata/pmetric" @@ -39,6 +38,7 @@ import ( "github.com/prometheus/prometheus/model/histogram" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/prompb" writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/prometheus/prometheus/storage" @@ -249,7 +249,7 @@ func (h *writeHandler) write(ctx context.Context, req *prompb.WriteRequest) (err // TODO(bwplotka): Even as per 1.0 spec, this should be a 400 error, while other samples are // potentially written. Perhaps unify with fixed writeV2 implementation a bit. - if !ls.Has(labels.MetricName) || !ls.IsValid(model.UTF8Validation) { + if !ls.Has(labels.MetricName) || !ls.IsValid(validation.UTF8NamingScheme) { h.logger.Warn("Invalid metric names or labels", "got", ls.String()) samplesWithInvalidLabels++ continue @@ -390,7 +390,7 @@ func (h *writeHandler) appendV2(app storage.Appender, req *writev2.Request, rs * // Validate series labels early. // NOTE(bwplotka): While spec allows UTF-8, Prometheus Receiver may impose // specific limits and follow https://prometheus.io/docs/specs/remote_write_spec_2_0/#invalid-samples case. - if !ls.Has(labels.MetricName) || !ls.IsValid(model.UTF8Validation) { + if !ls.Has(labels.MetricName) || !ls.IsValid(validation.UTF8NamingScheme) { badRequestErrs = append(badRequestErrs, fmt.Errorf("invalid metric name or labels, got %v", ls.String())) samplesWithInvalidLabels += len(ts.Samples) + len(ts.Histograms) continue diff --git a/vendor/github.com/prometheus/prometheus/web/api/v1/api.go b/vendor/github.com/prometheus/prometheus/web/api/v1/api.go index bb17c8a330e..b99c28416b9 100644 --- a/vendor/github.com/prometheus/prometheus/web/api/v1/api.go +++ b/vendor/github.com/prometheus/prometheus/web/api/v1/api.go @@ -45,6 +45,7 @@ import ( "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/metadata" "github.com/prometheus/prometheus/model/timestamp" + "github.com/prometheus/prometheus/model/validation" "github.com/prometheus/prometheus/promql" "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/rules" @@ -199,6 +200,7 @@ type API struct { Queryable storage.SampleAndChunkQueryable QueryEngine promql.QueryEngine ExemplarQueryable storage.ExemplarQueryable + NamingScheme validation.NamingScheme scrapePoolsRetriever func(context.Context) ScrapePoolsRetriever targetRetriever func(context.Context) TargetRetriever @@ -270,6 +272,7 @@ func NewAPI( QueryEngine: qe, Queryable: q, ExemplarQueryable: eq, + NamingScheme: validation.UTF8NamingScheme, scrapePoolsRetriever: spsr, targetRetriever: tr, @@ -782,8 +785,7 @@ func (api *API) labelValues(r *http.Request) (result apiFuncResult) { name = model.UnescapeName(name, model.ValueEncodingEscaping) } - label := model.LabelName(name) - if !label.IsValid() { + if !api.NamingScheme.IsValidLabelName(name) { return apiFuncResult{nil, &apiError{errorBadData, fmt.Errorf("invalid label name: %q", name)}, nil, nil} } diff --git a/vendor/modules.txt b/vendor/modules.txt index 8f908c8a6d0..989fb69c188 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1196,7 +1196,7 @@ github.com/prometheus/otlptranslator github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util -# github.com/prometheus/prometheus v1.99.0 => github.com/grafana/mimir-prometheus v1.8.2-0.20250611134813-2510b5041da2 +# github.com/prometheus/prometheus v1.99.0 => github.com/juliusmh/mimir-prometheus v1.8.2-0.20250618091651-809285202792 ## explicit; go 1.23.0 github.com/prometheus/prometheus/config github.com/prometheus/prometheus/discovery @@ -1210,6 +1210,7 @@ github.com/prometheus/prometheus/model/relabel github.com/prometheus/prometheus/model/rulefmt github.com/prometheus/prometheus/model/textparse github.com/prometheus/prometheus/model/timestamp +github.com/prometheus/prometheus/model/validation github.com/prometheus/prometheus/model/value github.com/prometheus/prometheus/notifier github.com/prometheus/prometheus/prompb @@ -2113,7 +2114,7 @@ sigs.k8s.io/kustomize/kyaml/yaml/walk sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 sigs.k8s.io/yaml/goyaml.v3 -# github.com/prometheus/prometheus => github.com/grafana/mimir-prometheus v1.8.2-0.20250611134813-2510b5041da2 +# github.com/prometheus/prometheus => github.com/juliusmh/mimir-prometheus v1.8.2-0.20250618091651-809285202792 # github.com/hashicorp/memberlist => github.com/grafana/memberlist v0.3.1-0.20250428154222-f7d51a6f6700 # gopkg.in/yaml.v3 => github.com/colega/go-yaml-yaml v0.0.0-20220720105220-255a8d16d094 # github.com/grafana/regexp => github.com/grafana/regexp v0.0.0-20240531075221-3685f1377d7b