diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 362939ddc..2727588c6 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -34,7 +34,7 @@ jobs: echo "supported_versions=$matrix" >> $GITHUB_OUTPUT test: - name: Tests (${{ matrix.go_version }}) + name: Tests (${{ matrix.go_version }}, ${{ matrix.validation_scheme }} name validation scheme) runs-on: ubuntu-latest needs: supportedVersions # Set fail-fast to false to ensure all Go versions are tested regardless of failures @@ -42,9 +42,10 @@ jobs: fail-fast: false matrix: go_version: ${{ fromJSON(needs.supportedVersions.outputs.supported_versions) }} + validation_scheme: ["global", "local"] # Define concurrency at the job level for matrix jobs concurrency: - group: ${{ github.workflow }}-test-${{ matrix.go_version }}-${{ (github.event.pull_request && github.event.pull_request.number) || github.ref || github.run_id }} + group: ${{ github.workflow }}-test-${{ matrix.go_version }}-${{ matrix.validation_scheme }}-${{ (github.event.pull_request && github.event.pull_request.number) || github.ref || github.run_id }} cancel-in-progress: true steps: @@ -62,7 +63,8 @@ jobs: run: make check_license test env: CI: true + GOOPTS: ${{ matrix.validation_scheme == 'local' && '-tags=localvalidationscheme' || '' }} - name: Run style and unused - if: ${{ matrix.go_version == '1.22' }} + if: ${{ matrix.go_version == '1.22' && matrix.validation_scheme == 'global' }} run: make style unused diff --git a/examples/exemplars/main.go b/examples/exemplars/main.go index 618224a7b..7625fdcaf 100644 --- a/examples/exemplars/main.go +++ b/examples/exemplars/main.go @@ -23,6 +23,8 @@ import ( "strconv" "time" + "github.com/prometheus/common/model" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -49,8 +51,11 @@ func main() { for { // Record fictional latency. now := time.Now() - requestDurations.(prometheus.ExemplarObserver).ObserveWithExemplar( - time.Since(now).Seconds(), prometheus.Labels{"dummyID": strconv.Itoa(rand.Intn(100000))}, + observeWithExemplar( + requestDurations.(prometheus.ExemplarObserver), + time.Since(now).Seconds(), + prometheus.Labels{"dummyID": strconv.Itoa(rand.Intn(100000))}, + model.UTF8Validation, ) time.Sleep(600 * time.Millisecond) } diff --git a/examples/exemplars/main_globalvalidationscheme.go b/examples/exemplars/main_globalvalidationscheme.go new file mode 100644 index 000000000..a7bbb3e95 --- /dev/null +++ b/examples/exemplars/main_globalvalidationscheme.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package main + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func observeWithExemplar(obs prometheus.ExemplarObserver, val float64, labels prometheus.Labels, _ model.ValidationScheme) { + obs.ObserveWithExemplar(val, labels) +} diff --git a/examples/exemplars/main_localvalidationscheme.go b/examples/exemplars/main_localvalidationscheme.go new file mode 100644 index 000000000..6ee78cbf1 --- /dev/null +++ b/examples/exemplars/main_localvalidationscheme.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package main + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func observeWithExemplar(obs prometheus.ExemplarObserver, val float64, labels prometheus.Labels, scheme model.ValidationScheme) { + obs.ObserveWithExemplar(val, labels, scheme) +} diff --git a/examples/random/main.go b/examples/random/main.go index 9192e94c2..55475d1df 100644 --- a/examples/random/main.go +++ b/examples/random/main.go @@ -25,6 +25,8 @@ import ( "strconv" "time" + "github.com/prometheus/common/model" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -115,8 +117,11 @@ func main() { // already know that rpcDurationsHistogram implements // the ExemplarObserver interface and thus don't need to // check the outcome of the type assertion. - m.rpcDurationsHistogram.(prometheus.ExemplarObserver).ObserveWithExemplar( - v, prometheus.Labels{"dummyID": strconv.Itoa(rand.Intn(100000))}, + observeWithExemplar( + m.rpcDurationsHistogram.(prometheus.ExemplarObserver), + v, + prometheus.Labels{"dummyID": strconv.Itoa(rand.Intn(100000))}, + model.UTF8Validation, ) time.Sleep(time.Duration(75*oscillationFactor()) * time.Millisecond) } diff --git a/examples/random/main_globalvalidationscheme.go b/examples/random/main_globalvalidationscheme.go new file mode 100644 index 000000000..a7bbb3e95 --- /dev/null +++ b/examples/random/main_globalvalidationscheme.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package main + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func observeWithExemplar(obs prometheus.ExemplarObserver, val float64, labels prometheus.Labels, _ model.ValidationScheme) { + obs.ObserveWithExemplar(val, labels) +} diff --git a/examples/random/main_localvalidationscheme.go b/examples/random/main_localvalidationscheme.go new file mode 100644 index 000000000..6ee78cbf1 --- /dev/null +++ b/examples/random/main_localvalidationscheme.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package main + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func observeWithExemplar(obs prometheus.ExemplarObserver, val float64, labels prometheus.Labels, scheme model.ValidationScheme) { + obs.ObserveWithExemplar(val, labels, scheme) +} diff --git a/go.mod b/go.mod index f724fe48e..227e729b7 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/klauspost/compress v1.18.0 github.com/kylelemons/godebug v1.1.0 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.65.0 + github.com/prometheus/common v0.65.1-0.20250714091050-c6ae72fb63e9 github.com/prometheus/procfs v0.16.1 go.uber.org/goleak v1.3.0 golang.org/x/sys v0.33.0 @@ -24,9 +24,9 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - golang.org/x/net v0.40.0 // indirect + golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index bc827d2a8..0a4fbc6b7 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -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/common v0.65.1-0.20250714091050-c6ae72fb63e9 h1:W+mk95PFPPi5NOzr2MtiGe7BXlHmsxs7UESIGsW5S08= +github.com/prometheus/common v0.65.1-0.20250714091050-c6ae72fb63e9/go.mod h1:41VB7D5p4TG2i2w5P4G62ofoS2mVyeTQ9QIAKYE60TE= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -48,14 +48,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/prometheus/counter.go b/prometheus/counter.go index 4ce84e7a8..eae99d3eb 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -20,6 +20,7 @@ import ( "time" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -44,19 +45,6 @@ type Counter interface { Add(float64) } -// ExemplarAdder is implemented by Counters that offer the option of adding a -// value to the Counter together with an exemplar. Its AddWithExemplar method -// works like the Add method of the Counter interface but also replaces the -// currently saved exemplar (if any) with a new one, created from the provided -// value, the current time as timestamp, and the provided labels. Empty Labels -// will lead to a valid (label-less) exemplar. But if Labels is nil, the current -// exemplar is left in place. AddWithExemplar panics if the value is < 0, if any -// of the provided labels are invalid, or if the provided labels contain more -// than 128 runes in total. -type ExemplarAdder interface { - AddWithExemplar(value float64, exemplar Labels) -} - // CounterOpts is an alias for Opts. See there for doc comments. type CounterOpts Opts @@ -143,11 +131,6 @@ func (c *counter) Add(v float64) { } } -func (c *counter) AddWithExemplar(v float64, e Labels) { - c.Add(v) - c.updateExemplar(v, e) -} - func (c *counter) Inc() { atomic.AddUint64(&c.valInt, 1) } @@ -169,11 +152,11 @@ func (c *counter) Write(out *dto.Metric) error { return populateMetric(CounterValue, val, c.labelPairs, exemplar, out, c.createdTs) } -func (c *counter) updateExemplar(v float64, l Labels) { +func (c *counter) updateExemplar(v float64, l Labels, scheme model.ValidationScheme) { if l == nil { return } - e, err := newExemplar(v, c.now(), l) + e, err := newExemplar(v, c.now(), l, scheme) if err != nil { panic(err) } diff --git a/prometheus/counter_globalvalidationscheme.go b/prometheus/counter_globalvalidationscheme.go new file mode 100644 index 000000000..0af448a3f --- /dev/null +++ b/prometheus/counter_globalvalidationscheme.go @@ -0,0 +1,37 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +// ExemplarAdder is implemented by Counters that offer the option of adding a +// value to the Counter together with an exemplar. Its AddWithExemplar method +// works like the Add method of the Counter interface but also replaces the +// currently saved exemplar (if any) with a new one, created from the provided +// value, the current time as timestamp, and the provided labels. Empty Labels +// will lead to a valid (label-less) exemplar. But if Labels is nil, the current +// exemplar is left in place. AddWithExemplar panics if the value is < 0, if any +// of the provided labels are invalid, or if the provided labels contain more +// than 128 runes in total. +type ExemplarAdder interface { + AddWithExemplar(value float64, exemplar Labels) +} + +func (c *counter) AddWithExemplar(v float64, e Labels) { + c.Add(v) + //nolint:staticcheck // model.NameValidationScheme is being phased out. + c.updateExemplar(v, e, model.NameValidationScheme) +} diff --git a/prometheus/counter_globalvalidationscheme_test.go b/prometheus/counter_globalvalidationscheme_test.go new file mode 100644 index 000000000..c6627ca30 --- /dev/null +++ b/prometheus/counter_globalvalidationscheme_test.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +func addToCounterWithExemplar(counter *counter, v float64, e Labels, _ model.ValidationScheme) { + counter.AddWithExemplar(v, e) +} diff --git a/prometheus/counter_localvalidationscheme.go b/prometheus/counter_localvalidationscheme.go new file mode 100644 index 000000000..76889440c --- /dev/null +++ b/prometheus/counter_localvalidationscheme.go @@ -0,0 +1,36 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +// ExemplarAdder is implemented by Counters that offer the option of adding a +// value to the Counter together with an exemplar. Its AddWithExemplar method +// works like the Add method of the Counter interface but also replaces the +// currently saved exemplar (if any) with a new one, created from the provided +// value, the current time as timestamp, and the provided labels. Empty Labels +// will lead to a valid (label-less) exemplar. But if Labels is nil, the current +// exemplar is left in place. AddWithExemplar panics if the value is < 0, if any +// of the provided labels are invalid, or if the provided labels contain more +// than 128 runes in total. +type ExemplarAdder interface { + AddWithExemplar(value float64, exemplar Labels, scheme model.ValidationScheme) +} + +func (c *counter) AddWithExemplar(v float64, e Labels, scheme model.ValidationScheme) { + c.Add(v) + c.updateExemplar(v, e, scheme) +} diff --git a/prometheus/counter_localvalidationscheme_test.go b/prometheus/counter_localvalidationscheme_test.go new file mode 100644 index 000000000..c0a8b1ab9 --- /dev/null +++ b/prometheus/counter_localvalidationscheme_test.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +func addToCounterWithExemplar(counter *counter, v float64, e Labels, scheme model.ValidationScheme) { + counter.AddWithExemplar(v, e, scheme) +} diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index 2b733494d..94b24da86 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -20,6 +20,7 @@ import ( "time" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -279,7 +280,7 @@ func TestCounterExemplar(t *testing.T) { Timestamp: ts, } - counter.AddWithExemplar(42, Labels{"foo": "bar"}) + addToCounterWithExemplar(counter, 42, Labels{"foo": "bar"}, model.UTF8Validation) if expected, got := expectedExemplar.String(), counter.exemplar.Load().(*dto.Exemplar).String(); expected != got { t.Errorf("expected exemplar %s, got %s.", expected, got) } @@ -291,7 +292,7 @@ func TestCounterExemplar(t *testing.T) { } }() // Should panic because of invalid label name. - counter.AddWithExemplar(42, Labels{"in\x80valid": "smile"}) + addToCounterWithExemplar(counter, 42, Labels{"in\x80valid": "smile"}, model.UTF8Validation) return nil } if addExemplarWithInvalidLabel() == nil { @@ -305,11 +306,11 @@ func TestCounterExemplar(t *testing.T) { } }() // Should panic because of 129 runes. - counter.AddWithExemplar(42, Labels{ + addToCounterWithExemplar(counter, 42, Labels{ "abcdefghijklmnopqrstuvwxyz": "26+16 characters", "x1234567": "8+15 characters", "z": strings.Repeat("x", 63), - }) + }, model.UTF8Validation) return nil } if addExemplarWithOversizedLabels() == nil { diff --git a/prometheus/desc.go b/prometheus/desc.go index ad347113c..70123325f 100644 --- a/prometheus/desc.go +++ b/prometheus/desc.go @@ -66,36 +66,13 @@ type Desc struct { err error } -// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc -// and will be reported on registration time. variableLabels and constLabels can -// be nil if no such labels should be set. fqName must not be empty. -// -// variableLabels only contain the label names. Their label values are variable -// and therefore not part of the Desc. (They are managed within the Metric.) -// -// For constLabels, the label values are constant. Therefore, they are fully -// specified in the Desc. See the Collector example for a usage pattern. -func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { - return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels) -} - -// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc -// and will be reported on registration time. variableLabels and constLabels can -// be nil if no such labels should be set. fqName must not be empty. -// -// variableLabels only contain the label names and normalization functions. Their -// label values are variable and therefore not part of the Desc. (They are managed -// within the Metric.) -// -// For constLabels, the label values are constant. Therefore, they are fully -// specified in the Desc. See the Collector example for a usage pattern. -func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc { +func newDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, scheme model.ValidationScheme) *Desc { d := &Desc{ fqName: fqName, help: help, variableLabels: variableLabels.compile(), } - if !model.IsValidMetricName(model.LabelValue(fqName)) { + if !isValidMetricName(model.LabelValue(fqName), scheme) { d.err = fmt.Errorf("%q is not a valid metric name", fqName) return d } @@ -107,7 +84,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const labelNameSet := map[string]struct{}{} // First add only the const label names and sort them... for labelName := range constLabels { - if !checkLabelName(labelName) { + if !checkLabelName(labelName, scheme) { d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName) return d } @@ -129,7 +106,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const // cannot be in a regular label name. That prevents matching the label // dimension with a different mix between preset and variable labels. for _, label := range d.variableLabels.names { - if !checkLabelName(label) { + if !checkLabelName(label, scheme) { d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName) return d } diff --git a/prometheus/desc_globalvalidationscheme.go b/prometheus/desc_globalvalidationscheme.go new file mode 100644 index 000000000..60e083d19 --- /dev/null +++ b/prometheus/desc_globalvalidationscheme.go @@ -0,0 +1,52 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import ( + "github.com/prometheus/common/model" +) + +// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc +// and will be reported on registration time. variableLabels and constLabels can +// be nil if no such labels should be set. fqName must not be empty. +// +// variableLabels only contain the label names. Their label values are variable +// and therefore not part of the Desc. (They are managed within the Metric.) +// +// For constLabels, the label values are constant. Therefore, they are fully +// specified in the Desc. See the Collector example for a usage pattern. +func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { + return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels) +} + +// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc +// and will be reported on registration time. variableLabels and constLabels can +// be nil if no such labels should be set. fqName must not be empty. +// +// variableLabels only contain the label names and normalization functions. Their +// label values are variable and therefore not part of the Desc. (They are managed +// within the Metric.) +// +// For constLabels, the label values are constant. Therefore, they are fully +// specified in the Desc. See the Collector example for a usage pattern. +func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc { + //nolint:staticcheck // model.NameValidationScheme is being phased out. + return newDesc(fqName, help, variableLabels, constLabels, model.NameValidationScheme) +} + +func isValidMetricName(metricName model.LabelValue, _ model.ValidationScheme) bool { + return model.IsValidMetricName(metricName) +} diff --git a/prometheus/desc_localvalidationscheme.go b/prometheus/desc_localvalidationscheme.go new file mode 100644 index 000000000..077d7af05 --- /dev/null +++ b/prometheus/desc_localvalidationscheme.go @@ -0,0 +1,68 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import ( + "github.com/prometheus/common/model" +) + +type descriptorOptions struct { + validationScheme model.ValidationScheme +} + +// newDescriptorOptions creates default descriptor options and applies opts. +func newDescriptorOptions(opts ...DescOption) *descriptorOptions { + d := &descriptorOptions{ + validationScheme: model.UTF8Validation, + } + for _, o := range opts { + o(d) + } + return d +} + +// WithValidationScheme ensures descriptor's label and metric names adhere to scheme. +// Default is UTF-8 validation. +func WithValidationScheme(scheme model.ValidationScheme) DescOption { + return func(o *descriptorOptions) { + o.validationScheme = scheme + } +} + +// DescOption are options that can be passed to NewDesc +type DescOption func(*descriptorOptions) + +// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc +// and will be reported on registration time. variableLabels and constLabels can +// be nil if no such labels should be set. fqName must not be empty. +// +// variableLabels only contain the label names. Their label values are variable +// and therefore not part of the Desc. (They are managed within the Metric.) +// +// For constLabels, the label values are constant. Therefore, they are fully +// specified in the Desc. See the Collector example for a usage pattern. +func NewDesc(fqName, help string, variableLabels []string, constLabels Labels, opts ...DescOption) *Desc { + return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels, opts...) +} + +func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, opts ...DescOption) *Desc { + descOpts := newDescriptorOptions(opts...) + return newDesc(fqName, help, variableLabels, constLabels, descOpts.validationScheme) +} + +func isValidMetricName(metricName model.LabelValue, scheme model.ValidationScheme) bool { + return model.IsValidMetricName(metricName, scheme) +} diff --git a/prometheus/desc_localvalidationscheme_test.go b/prometheus/desc_localvalidationscheme_test.go new file mode 100644 index 000000000..8a9d57887 --- /dev/null +++ b/prometheus/desc_localvalidationscheme_test.go @@ -0,0 +1,69 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import ( + "testing" + + "github.com/prometheus/common/model" +) + +func TestNewDesc_WithValidationScheme(t *testing.T) { + testCases := []struct { + name string + fqName string + help string + variableLabels []string + labels Labels + opts []DescOption + wantErr string + }{ + { + name: "invalid legacy label name", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"test😀": "test"}, + opts: []DescOption{WithValidationScheme(model.LegacyValidation)}, + wantErr: `"test😀" is not a valid label name for metric "sample_label"`, + }, + { + name: "invalid legacy metric name", + fqName: "sample_label😀", + help: "sample label", + opts: []DescOption{WithValidationScheme(model.LegacyValidation)}, + wantErr: `"sample_label😀" is not a valid metric name`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + desc := NewDesc( + tc.fqName, + tc.help, + tc.variableLabels, + tc.labels, + tc.opts..., + ) + if desc.err != nil && tc.wantErr != desc.err.Error() { + t.Fatalf("NewDesc: expected error %q but got %+v", tc.wantErr, desc.err) + } else if desc.err == nil && tc.wantErr != "" { + t.Fatalf("NewDesc: expected error %q but got nil", tc.wantErr) + } else if desc.err != nil && tc.wantErr == "" { + t.Fatalf("NewDesc: %+v", desc.err) + } + }) + } +} diff --git a/prometheus/desc_test.go b/prometheus/desc_test.go index 5a8429009..c0316055f 100644 --- a/prometheus/desc_test.go +++ b/prometheus/desc_test.go @@ -17,27 +17,62 @@ import ( "testing" ) -func TestNewDescInvalidLabelValues(t *testing.T) { - desc := NewDesc( - "sample_label", - "sample label", - nil, - Labels{"a": "\xFF"}, - ) - if desc.err == nil { - t.Errorf("NewDesc: expected error because: %s", desc.err) +func TestNewDesc(t *testing.T) { + testCases := []struct { + name string + fqName string + help string + variableLabels []string + labels Labels + wantErr string + }{ + { + name: "invalid label value", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"a": "\xff"}, + wantErr: `label value "\xff" is not valid UTF-8`, + }, + { + name: "nil label values", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: nil, + }, + { + name: "invalid label name", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"\xff": "test"}, + wantErr: `"\xff" is not a valid label name for metric "sample_label"`, + }, + { + name: "valid utf8 label name", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"test😀": "test"}, + }, } -} - -func TestNewDescNilLabelValues(t *testing.T) { - desc := NewDesc( - "sample_label", - "sample label", - nil, - nil, - ) - if desc.err != nil { - t.Errorf("NewDesc: unexpected error: %s", desc.err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + desc := NewDesc( + tc.fqName, + tc.help, + tc.variableLabels, + tc.labels, + ) + if desc.err != nil && tc.wantErr != desc.err.Error() { + t.Fatalf("NewDesc: expected error %q but got %+v", tc.wantErr, desc.err) + } else if desc.err == nil && tc.wantErr != "" { + t.Fatalf("NewDesc: expected error %q but got nil", tc.wantErr) + } else if desc.err != nil && tc.wantErr == "" { + t.Fatalf("NewDesc: %+v", desc.err) + } + }) } } diff --git a/prometheus/examples_globalvalidationscheme_test.go b/prometheus/examples_globalvalidationscheme_test.go new file mode 100644 index 000000000..f84e0b6c0 --- /dev/null +++ b/prometheus/examples_globalvalidationscheme_test.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus_test + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func mustNewMetricWithExemplars(m prometheus.Metric, _ model.ValidationScheme, exemplars ...prometheus.Exemplar) prometheus.Metric { + return prometheus.MustNewMetricWithExemplars(m, exemplars...) +} diff --git a/prometheus/examples_localvalidationscheme_test.go b/prometheus/examples_localvalidationscheme_test.go new file mode 100644 index 000000000..1e649c971 --- /dev/null +++ b/prometheus/examples_localvalidationscheme_test.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus_test + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func mustNewMetricWithExemplars(m prometheus.Metric, scheme model.ValidationScheme, exemplars ...prometheus.Exemplar) prometheus.Metric { + return prometheus.MustNewMetricWithExemplars(m, scheme, exemplars...) +} diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index cc179ae7c..80f590d1d 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -25,6 +25,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -533,8 +534,9 @@ func ExampleNewConstHistogram_withExemplar() { // Wrap const histogram with exemplars for each bucket. exemplarTs, _ := time.Parse(time.RFC850, "Monday, 02-Jan-06 15:04:05 GMT") exemplarLabels := prometheus.Labels{"testName": "testVal"} - h = prometheus.MustNewMetricWithExemplars( + h = mustNewMetricWithExemplars( h, + model.UTF8Validation, prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 24.0}, prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 42.0}, prometheus.Exemplar{Labels: exemplarLabels, Timestamp: exemplarTs, Value: 89.0}, @@ -608,10 +610,10 @@ temperature_kelvin{location="somewhere else"} 4.5 return result, nil } - gatherers := prometheus.Gatherers{ + gatherers := newGatherers([]prometheus.Gatherer{ reg, prometheus.GathererFunc(parseText), - } + }, model.UTF8Validation) gathering, err := gatherers.Gather() if err != nil { diff --git a/prometheus/histogram.go b/prometheus/histogram.go index c453b754a..23e36e5b3 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -24,6 +24,7 @@ import ( "time" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -767,15 +768,6 @@ func (h *histogram) Observe(v float64) { h.observe(v, h.findBucket(v)) } -// ObserveWithExemplar should not be called in a high-frequency setting -// for a native histogram with configured exemplars. For this case, -// the implementation isn't lock-free and might suffer from lock contention. -func (h *histogram) ObserveWithExemplar(v float64, e Labels) { - i := h.findBucket(v) - h.observe(v, i) - h.updateExemplar(v, i, e) -} - func (h *histogram) Write(out *dto.Metric) error { // For simplicity, we protect this whole method by a mutex. It is not in // the hot path, i.e. Observe is called much more often than Write. The @@ -1150,11 +1142,11 @@ func (h *histogram) resetCounts(counts *histogramCounts) { // With empty labels, it's a no-op. It panics if any of the labels is invalid. // If histogram is native, the exemplar will be cached into nativeExemplars, // which has a limit, and will remove one exemplar when limit is reached. -func (h *histogram) updateExemplar(v float64, bucket int, l Labels) { +func (h *histogram) updateExemplar(v float64, bucket int, l Labels, scheme model.ValidationScheme) { if l == nil { return } - e, err := newExemplar(v, h.now(), l) + e, err := newExemplar(v, h.now(), l, scheme) if err != nil { panic(err) } diff --git a/prometheus/histogram_globalvalidationscheme.go b/prometheus/histogram_globalvalidationscheme.go new file mode 100644 index 000000000..e527b7c05 --- /dev/null +++ b/prometheus/histogram_globalvalidationscheme.go @@ -0,0 +1,28 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +// ObserveWithExemplar should not be called in a high-frequency setting +// for a native histogram with configured exemplars. For this case, +// the implementation isn't lock-free and might suffer from lock contention. +func (h *histogram) ObserveWithExemplar(v float64, e Labels) { + i := h.findBucket(v) + h.observe(v, i) + //nolint:staticcheck // model.NameValidationScheme is being phased out. + h.updateExemplar(v, i, e, model.NameValidationScheme) +} diff --git a/prometheus/histogram_globalvalidationscheme_test.go b/prometheus/histogram_globalvalidationscheme_test.go new file mode 100644 index 000000000..ce9ea6a29 --- /dev/null +++ b/prometheus/histogram_globalvalidationscheme_test.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +func observeWithExemplar(observer ExemplarObserver, value float64, exemplar Labels, _ model.ValidationScheme) { + observer.ObserveWithExemplar(value, exemplar) +} diff --git a/prometheus/histogram_localvalidationscheme.go b/prometheus/histogram_localvalidationscheme.go new file mode 100644 index 000000000..3d207315d --- /dev/null +++ b/prometheus/histogram_localvalidationscheme.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +// ObserveWithExemplar should not be called in a high-frequency setting +// for a native histogram with configured exemplars. For this case, +// the implementation isn't lock-free and might suffer from lock contention. +func (h *histogram) ObserveWithExemplar(v float64, e Labels, scheme model.ValidationScheme) { + i := h.findBucket(v) + h.observe(v, i) + h.updateExemplar(v, i, e, scheme) +} diff --git a/prometheus/histogram_localvalidationscheme_test.go b/prometheus/histogram_localvalidationscheme_test.go new file mode 100644 index 000000000..1b352be68 --- /dev/null +++ b/prometheus/histogram_localvalidationscheme_test.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +func observeWithExemplar(observer ExemplarObserver, value float64, exemplar Labels, scheme model.ValidationScheme) { + observer.ObserveWithExemplar(value, exemplar, scheme) +} diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index 1a19df2e2..2a916ca96 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -26,6 +26,7 @@ import ( "time" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -191,7 +192,7 @@ func TestHistogramConcurrency(t *testing.T) { if n%2 == 0 { his.Observe(v) } else { - his.(ExemplarObserver).ObserveWithExemplar(v, Labels{"foo": "bar"}) + observeWithExemplar(his.(ExemplarObserver), v, Labels{"foo": "bar"}, model.UTF8Validation) } } end.Done() @@ -450,10 +451,10 @@ func TestHistogramExemplar(t *testing.T) { }, } - histogram.ObserveWithExemplar(1.5, Labels{"id": "1"}) - histogram.ObserveWithExemplar(1.6, Labels{"id": "2"}) // To replace exemplar in bucket 0. - histogram.ObserveWithExemplar(4, Labels{"id": "3"}) - histogram.ObserveWithExemplar(4.5, Labels{"id": "4"}) // Should go to +Inf bucket. + observeWithExemplar(histogram, 1.5, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(histogram, 1.6, Labels{"id": "2"}, model.UTF8Validation) // To replace exemplar in bucket 0. + observeWithExemplar(histogram, 4, Labels{"id": "3"}, model.UTF8Validation) + observeWithExemplar(histogram, 4.5, Labels{"id": "4"}, model.UTF8Validation) // Should go to +Inf bucket. for i, ex := range histogram.exemplars { var got, expected string @@ -1056,7 +1057,7 @@ func TestNativeHistogramConcurrency(t *testing.T) { if i%2 == 0 { his.Observe(v) } else { - his.(ExemplarObserver).ObserveWithExemplar(v, Labels{"foo": "bar"}) + observeWithExemplar(his.(ExemplarObserver), v, Labels{"foo": "bar"}, model.UTF8Validation) } } end.Done() @@ -1321,23 +1322,23 @@ func TestNativeHistogramExemplar(t *testing.T) { { name: "add exemplars to the limit", addFunc: func(h *histogram) { - h.ObserveWithExemplar(1, Labels{"id": "1"}) - h.ObserveWithExemplar(3, Labels{"id": "1"}) - h.ObserveWithExemplar(5, Labels{"id": "1"}) + observeWithExemplar(h, 1, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(h, 3, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(h, 5, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{1, 3, 5}, }, { name: "remove exemplar in closest pair, the removed index equals to inserted index", addFunc: func(h *histogram) { - h.ObserveWithExemplar(4, Labels{"id": "1"}) + observeWithExemplar(h, 4, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{1, 3, 4}, }, { name: "remove exemplar in closest pair, the removed index is bigger than inserted index", addFunc: func(h *histogram) { - h.ObserveWithExemplar(0, Labels{"id": "1"}) + observeWithExemplar(h, 0, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{0, 1, 4}, }, @@ -1345,7 +1346,7 @@ func TestNativeHistogramExemplar(t *testing.T) { name: "remove exemplar with oldest timestamp, the removed index is smaller than inserted index", addFunc: func(h *histogram) { h.now = func() time.Time { return time.Now().Add(time.Second * 11) } - h.ObserveWithExemplar(6, Labels{"id": "1"}) + observeWithExemplar(h, 6, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{0, 4, 6}, }, @@ -1376,30 +1377,30 @@ func TestNativeHistogramExemplar(t *testing.T) { { name: "add exemplars to the limit", addFunc: func(h *histogram) { - h.ObserveWithExemplar(1, Labels{"id": "1"}) - h.ObserveWithExemplar(3, Labels{"id": "1"}) - h.ObserveWithExemplar(5, Labels{"id": "1"}) + observeWithExemplar(h, 1, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(h, 3, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(h, 5, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{1, 3, 5}, }, { name: "remove exemplar with oldest timestamp, the removed index is smaller than inserted index", addFunc: func(h *histogram) { - h.ObserveWithExemplar(4, Labels{"id": "1"}) + observeWithExemplar(h, 4, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{3, 4, 5}, }, { name: "remove exemplar with oldest timestamp, the removed index equals to inserted index", addFunc: func(h *histogram) { - h.ObserveWithExemplar(0, Labels{"id": "1"}) + observeWithExemplar(h, 0, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{0, 4, 5}, }, { name: "remove exemplar with oldest timestamp, the removed index is bigger than inserted index", addFunc: func(h *histogram) { - h.ObserveWithExemplar(3, Labels{"id": "1"}) + observeWithExemplar(h, 3, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{0, 3, 4}, }, @@ -1430,9 +1431,9 @@ func TestNativeHistogramExemplar(t *testing.T) { { name: "add exemplars to the limit, but no effect", addFunc: func(h *histogram) { - h.ObserveWithExemplar(1, Labels{"id": "1"}) - h.ObserveWithExemplar(3, Labels{"id": "1"}) - h.ObserveWithExemplar(5, Labels{"id": "1"}) + observeWithExemplar(h, 1, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(h, 3, Labels{"id": "1"}, model.UTF8Validation) + observeWithExemplar(h, 5, Labels{"id": "1"}, model.UTF8Validation) }, expectedValues: []float64{}, }, diff --git a/prometheus/labels.go b/prometheus/labels.go index c21911f29..913ac6044 100644 --- a/prometheus/labels.go +++ b/prometheus/labels.go @@ -16,10 +16,7 @@ package prometheus import ( "errors" "fmt" - "strings" "unicode/utf8" - - "github.com/prometheus/common/model" ) // Labels represents a collection of label name -> value mappings. This type is @@ -182,7 +179,3 @@ func validateLabelValues(vals []string, expectedNumberOfValues int) error { return nil } - -func checkLabelName(l string) bool { - return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix) -} diff --git a/prometheus/labels_globalvalidationscheme.go b/prometheus/labels_globalvalidationscheme.go new file mode 100644 index 000000000..c6b3d4b1a --- /dev/null +++ b/prometheus/labels_globalvalidationscheme.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import ( + "strings" + + "github.com/prometheus/common/model" +) + +func checkLabelName(l string, _ model.ValidationScheme) bool { + return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix) +} diff --git a/prometheus/labels_localvalidationscheme.go b/prometheus/labels_localvalidationscheme.go new file mode 100644 index 000000000..5061d67a1 --- /dev/null +++ b/prometheus/labels_localvalidationscheme.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import ( + "strings" + + "github.com/prometheus/common/model" +) + +func checkLabelName(l string, scheme model.ValidationScheme) bool { + return model.LabelName(l).IsValid(scheme) && !strings.HasPrefix(l, reservedLabelPrefix) +} diff --git a/prometheus/metric.go b/prometheus/metric.go index 76e59f128..0dd7278d4 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -231,17 +231,7 @@ type Exemplar struct { Timestamp time.Time } -// NewMetricWithExemplars returns a new Metric wrapping the provided Metric with given -// exemplars. Exemplars are validated. -// -// Only last applicable exemplar is injected from the list. -// For example for Counter it means last exemplar is injected. -// For Histogram, it means last applicable exemplar for each bucket is injected. -// For a Native Histogram, all valid exemplars are injected. -// -// NewMetricWithExemplars works best with MustNewConstMetric and -// MustNewConstHistogram, see example. -func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) { +func newMetricWithExemplars(m Metric, scheme model.ValidationScheme, exemplars ...Exemplar) (Metric, error) { if len(exemplars) == 0 { return nil, errors.New("no exemplar was passed for NewMetricWithExemplars") } @@ -256,7 +246,7 @@ func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) { if ts.IsZero() { ts = now } - exs[i], err = newExemplar(e.Value, ts, e.Labels) + exs[i], err = newExemplar(e.Value, ts, e.Labels, scheme) if err != nil { return nil, err } @@ -264,13 +254,3 @@ func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) { return &withExemplarsMetric{Metric: m, exemplars: exs}, nil } - -// MustNewMetricWithExemplars is a version of NewMetricWithExemplars that panics where -// NewMetricWithExemplars would have returned an error. -func MustNewMetricWithExemplars(m Metric, exemplars ...Exemplar) Metric { - ret, err := NewMetricWithExemplars(m, exemplars...) - if err != nil { - panic(err) - } - return ret -} diff --git a/prometheus/metric_globalvalidationscheme.go b/prometheus/metric_globalvalidationscheme.go new file mode 100644 index 000000000..0788a9c7c --- /dev/null +++ b/prometheus/metric_globalvalidationscheme.go @@ -0,0 +1,45 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import ( + "github.com/prometheus/common/model" +) + +// NewMetricWithExemplars returns a new Metric wrapping the provided Metric with given +// exemplars. Exemplars are validated. +// +// Only last applicable exemplar is injected from the list. +// For example for Counter it means last exemplar is injected. +// For Histogram, it means last applicable exemplar for each bucket is injected. +// For a Native Histogram, all valid exemplars are injected. +// +// NewMetricWithExemplars works best with MustNewConstMetric and +// MustNewConstHistogram, see example. +func NewMetricWithExemplars(m Metric, exemplars ...Exemplar) (Metric, error) { + //nolint:staticcheck // model.NameValidationScheme is being phased out. + return newMetricWithExemplars(m, model.NameValidationScheme, exemplars...) +} + +// MustNewMetricWithExemplars is a version of NewMetricWithExemplars that panics where +// NewMetricWithExemplars would have returned an error. +func MustNewMetricWithExemplars(m Metric, exemplars ...Exemplar) Metric { + ret, err := NewMetricWithExemplars(m, exemplars...) + if err != nil { + panic(err) + } + return ret +} diff --git a/prometheus/metric_globalvalidationscheme_test.go b/prometheus/metric_globalvalidationscheme_test.go new file mode 100644 index 000000000..0eeb2b986 --- /dev/null +++ b/prometheus/metric_globalvalidationscheme_test.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +func mustNewMetricWithExemplars(m Metric, _ model.ValidationScheme, exemplars ...Exemplar) Metric { + return MustNewMetricWithExemplars(m, exemplars...) +} diff --git a/prometheus/metric_localvalidationscheme.go b/prometheus/metric_localvalidationscheme.go new file mode 100644 index 000000000..76dca94e2 --- /dev/null +++ b/prometheus/metric_localvalidationscheme.go @@ -0,0 +1,44 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import ( + "github.com/prometheus/common/model" +) + +// NewMetricWithExemplars returns a new Metric wrapping the provided Metric with given +// exemplars. Exemplars are validated. +// +// Only last applicable exemplar is injected from the list. +// For example for Counter it means last exemplar is injected. +// For Histogram, it means last applicable exemplar for each bucket is injected. +// For a Native Histogram, all valid exemplars are injected. +// +// NewMetricWithExemplars works best with MustNewConstMetric and +// MustNewConstHistogram, see example. +func NewMetricWithExemplars(m Metric, scheme model.ValidationScheme, exemplars ...Exemplar) (Metric, error) { + return newMetricWithExemplars(m, scheme, exemplars...) +} + +// MustNewMetricWithExemplars is a version of NewMetricWithExemplars that panics where +// NewMetricWithExemplars would have returned an error. +func MustNewMetricWithExemplars(m Metric, scheme model.ValidationScheme, exemplars ...Exemplar) Metric { + ret, err := NewMetricWithExemplars(m, scheme, exemplars...) + if err != nil { + panic(err) + } + return ret +} diff --git a/prometheus/metric_localvalidationscheme_test.go b/prometheus/metric_localvalidationscheme_test.go new file mode 100644 index 000000000..5db063871 --- /dev/null +++ b/prometheus/metric_localvalidationscheme_test.go @@ -0,0 +1,22 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +func mustNewMetricWithExemplars(m Metric, scheme model.ValidationScheme, exemplars ...Exemplar) Metric { + return MustNewMetricWithExemplars(m, scheme, exemplars...) +} diff --git a/prometheus/metric_test.go b/prometheus/metric_test.go index f6553c332..d73a2532f 100644 --- a/prometheus/metric_test.go +++ b/prometheus/metric_test.go @@ -21,6 +21,7 @@ import ( "time" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -149,7 +150,7 @@ func TestWithExemplarsNativeHistogramMetric(t *testing.T) { NewDesc("http_request_duration_seconds", "A histogram of the HTTP request durations.", nil, nil), 10, 12.1, map[int]int64{1: 7, 2: 1, 3: 2}, map[int]int64{}, 0, 2, 0.2, time.Date( 2009, 11, 17, 20, 34, 58, 651387237, time.UTC)) - m := MustNewMetricWithExemplars(h, Exemplar{ + m := mustNewMetricWithExemplars(h, model.UTF8Validation, Exemplar{ Value: 1000.0, }) metric := dto.Metric{} @@ -272,10 +273,7 @@ func TestWithExemplarsNativeHistogramMetric(t *testing.T) { if err != nil { t.Fail() } - metricWithExemplar, err := NewMetricWithExemplars(m, tc.Exemplars[0]) - if err != nil { - t.Fail() - } + metricWithExemplar := mustNewMetricWithExemplars(m, model.UTF8Validation, tc.Exemplars[0]) got := &dto.Metric{} err = metricWithExemplar.Write(got) if err != nil { diff --git a/prometheus/observer.go b/prometheus/observer.go index 03773b21f..5806cd09e 100644 --- a/prometheus/observer.go +++ b/prometheus/observer.go @@ -50,15 +50,3 @@ type ObserverVec interface { Collector } - -// ExemplarObserver is implemented by Observers that offer the option of -// observing a value together with an exemplar. Its ObserveWithExemplar method -// works like the Observe method of an Observer but also replaces the currently -// saved exemplar (if any) with a new one, created from the provided value, the -// current time as timestamp, and the provided Labels. Empty Labels will lead to -// a valid (label-less) exemplar. But if Labels is nil, the current exemplar is -// left in place. ObserveWithExemplar panics if any of the provided labels are -// invalid or if the provided labels contain more than 128 runes in total. -type ExemplarObserver interface { - ObserveWithExemplar(value float64, exemplar Labels) -} diff --git a/prometheus/observer_globalvalidationscheme.go b/prometheus/observer_globalvalidationscheme.go new file mode 100644 index 000000000..303fbe337 --- /dev/null +++ b/prometheus/observer_globalvalidationscheme.go @@ -0,0 +1,28 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +// ExemplarObserver is implemented by Observers that offer the option of +// observing a value together with an exemplar. Its ObserveWithExemplar method +// works like the Observe method of an Observer but also replaces the currently +// saved exemplar (if any) with a new one, created from the provided value, the +// current time as timestamp, and the provided Labels. Empty Labels will lead to +// a valid (label-less) exemplar. But if Labels is nil, the current exemplar is +// left in place. ObserveWithExemplar panics if any of the provided labels are +// invalid or if the provided labels contain more than 128 runes in total. +type ExemplarObserver interface { + ObserveWithExemplar(value float64, exemplar Labels) +} diff --git a/prometheus/observer_localvalidationscheme.go b/prometheus/observer_localvalidationscheme.go new file mode 100644 index 000000000..bd6b685b3 --- /dev/null +++ b/prometheus/observer_localvalidationscheme.go @@ -0,0 +1,30 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import "github.com/prometheus/common/model" + +// ExemplarObserver is implemented by Observers that offer the option of +// observing a value together with an exemplar. Its ObserveWithExemplar method +// works like the Observe method of an Observer but also replaces the currently +// saved exemplar (if any) with a new one, created from the provided value, the +// current time as timestamp, and the provided Labels. Empty Labels will lead to +// a valid (label-less) exemplar. But if Labels is nil, the current exemplar is +// left in place. ObserveWithExemplar panics if any of the provided labels are +// invalid or if the provided labels contain more than 128 runes in total. +type ExemplarObserver interface { + ObserveWithExemplar(value float64, exemplar Labels, scheme model.ValidationScheme) +} diff --git a/prometheus/promhttp/instrument_client.go b/prometheus/promhttp/instrument_client.go index d3482c40c..4fcf4cddb 100644 --- a/prometheus/promhttp/instrument_client.go +++ b/prometheus/promhttp/instrument_client.go @@ -78,7 +78,7 @@ func InstrumentRoundTripperCounter(counter *prometheus.CounterVec, next http.Rou for label, resolve := range rtOpts.extraLabelsFromCtx { l[label] = resolve(resp.Request.Context()) } - addWithExemplar(counter.With(l), 1, rtOpts.getExemplarFn(r.Context())) + addWithExemplar(counter.With(l), 1, rtOpts.getExemplarFn(r.Context()), rtOpts.validationScheme) } return resp, err } @@ -122,7 +122,7 @@ func InstrumentRoundTripperDuration(obs prometheus.ObserverVec, next http.RoundT for label, resolve := range rtOpts.extraLabelsFromCtx { l[label] = resolve(resp.Request.Context()) } - observeWithExemplar(obs.With(l), time.Since(start).Seconds(), rtOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(start).Seconds(), rtOpts.getExemplarFn(r.Context()), rtOpts.validationScheme) } return resp, err } diff --git a/prometheus/promhttp/instrument_server.go b/prometheus/promhttp/instrument_server.go index 9332b0249..70f113b7a 100644 --- a/prometheus/promhttp/instrument_server.go +++ b/prometheus/promhttp/instrument_server.go @@ -28,26 +28,6 @@ import ( // magicString is used for the hacky label test in checkLabels. Remove once fixed. const magicString = "zZgWfBxLqvG8kc8IMv3POi2Bb0tZI3vAnBx+gBaFi9FyPzB/CzKUer1yufDa" -// observeWithExemplar is a wrapper for [prometheus.ExemplarAdder.ExemplarObserver], -// which falls back to [prometheus.Observer.Observe] if no labels are provided. -func observeWithExemplar(obs prometheus.Observer, val float64, labels map[string]string) { - if labels == nil { - obs.Observe(val) - return - } - obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels) -} - -// addWithExemplar is a wrapper for [prometheus.ExemplarAdder.AddWithExemplar], -// which falls back to [prometheus.Counter.Add] if no labels are provided. -func addWithExemplar(obs prometheus.Counter, val float64, labels map[string]string) { - if labels == nil { - obs.Add(val) - return - } - obs.(prometheus.ExemplarAdder).AddWithExemplar(val, labels) -} - // InstrumentHandlerInFlight is a middleware that wraps the provided // http.Handler. It sets the provided prometheus.Gauge to the number of // requests currently handled by the wrapped http.Handler. @@ -100,7 +80,7 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) } } @@ -111,7 +91,7 @@ func InstrumentHandlerDuration(obs prometheus.ObserverVec, next http.Handler, op for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) } } @@ -150,7 +130,7 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context())) + addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) } } @@ -161,7 +141,7 @@ func InstrumentHandlerCounter(counter *prometheus.CounterVec, next http.Handler, for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context())) + addWithExemplar(counter.With(l), 1, hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) } } @@ -203,7 +183,7 @@ func InstrumentHandlerTimeToWriteHeader(obs prometheus.ObserverVec, next http.Ha for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), time.Since(now).Seconds(), hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) }) next.ServeHTTP(d, r) } @@ -247,7 +227,7 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) } } @@ -259,7 +239,7 @@ func InstrumentHandlerRequestSize(obs prometheus.ObserverVec, next http.Handler, for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), float64(size), hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) } } @@ -299,7 +279,7 @@ func InstrumentHandlerResponseSize(obs prometheus.ObserverVec, next http.Handler for label, resolve := range hOpts.extraLabelsFromCtx { l[label] = resolve(r.Context()) } - observeWithExemplar(obs.With(l), float64(d.Written()), hOpts.getExemplarFn(r.Context())) + observeWithExemplar(obs.With(l), float64(d.Written()), hOpts.getExemplarFn(r.Context()), hOpts.validationScheme) }) } diff --git a/prometheus/promhttp/instrument_server_globalvalidationscheme.go b/prometheus/promhttp/instrument_server_globalvalidationscheme.go new file mode 100644 index 000000000..a1f1aaf14 --- /dev/null +++ b/prometheus/promhttp/instrument_server_globalvalidationscheme.go @@ -0,0 +1,42 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package promhttp + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +// observeWithExemplar is a wrapper for [prometheus.ExemplarAdder.ExemplarObserver], +// which falls back to [prometheus.Observer.Observe] if no labels are provided. +func observeWithExemplar(obs prometheus.Observer, val float64, labels map[string]string, _ model.ValidationScheme) { + if labels == nil { + obs.Observe(val) + return + } + obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels) +} + +// addWithExemplar is a wrapper for [prometheus.ExemplarAdder.AddWithExemplar], +// which falls back to [prometheus.Counter.Add] if no labels are provided. +func addWithExemplar(obs prometheus.Counter, val float64, labels map[string]string, _ model.ValidationScheme) { + if labels == nil { + obs.Add(val) + return + } + obs.(prometheus.ExemplarAdder).AddWithExemplar(val, labels) +} diff --git a/prometheus/promhttp/instrument_server_localvalidationscheme.go b/prometheus/promhttp/instrument_server_localvalidationscheme.go new file mode 100644 index 000000000..b45c8dba3 --- /dev/null +++ b/prometheus/promhttp/instrument_server_localvalidationscheme.go @@ -0,0 +1,45 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package promhttp + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +// observeWithExemplar is a wrapper for [prometheus.ExemplarAdder.ExemplarObserver], +// which falls back to [prometheus.Observer.Observe] if no labels are provided. +func observeWithExemplar(obs prometheus.Observer, val float64, labels map[string]string, scheme model.ValidationScheme) { + if scheme == model.UnsetValidation { + scheme = model.UTF8Validation + } + if labels == nil { + obs.Observe(val) + return + } + obs.(prometheus.ExemplarObserver).ObserveWithExemplar(val, labels, scheme) +} + +// addWithExemplar is a wrapper for [prometheus.ExemplarAdder.AddWithExemplar], +// which falls back to [prometheus.Counter.Add] if no labels are provided. +func addWithExemplar(obs prometheus.Counter, val float64, labels map[string]string, scheme model.ValidationScheme) { + if labels == nil { + obs.Add(val) + return + } + obs.(prometheus.ExemplarAdder).AddWithExemplar(val, labels, scheme) +} diff --git a/prometheus/promhttp/option.go b/prometheus/promhttp/option.go index 5d4383aa1..abe782434 100644 --- a/prometheus/promhttp/option.go +++ b/prometheus/promhttp/option.go @@ -16,6 +16,8 @@ package promhttp import ( "context" + "github.com/prometheus/common/model" + "github.com/prometheus/client_golang/prometheus" ) @@ -33,12 +35,14 @@ type options struct { extraMethods []string getExemplarFn func(requestCtx context.Context) prometheus.Labels extraLabelsFromCtx map[string]LabelValueFromCtx + validationScheme model.ValidationScheme } func defaultOptions() *options { return &options{ getExemplarFn: func(ctx context.Context) prometheus.Labels { return nil }, extraLabelsFromCtx: map[string]LabelValueFromCtx{}, + validationScheme: model.UTF8Validation, } } diff --git a/prometheus/promhttp/option_localvalidationscheme.go b/prometheus/promhttp/option_localvalidationscheme.go new file mode 100644 index 000000000..b07941f9d --- /dev/null +++ b/prometheus/promhttp/option_localvalidationscheme.go @@ -0,0 +1,25 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package promhttp + +import "github.com/prometheus/common/model" + +// WithNameValidationScheme registers the name validation scheme to use. +func WithNameValidationScheme(name string, scheme model.ValidationScheme) Option { + return optionApplyFunc(func(o *options) { + o.validationScheme = scheme + }) +} diff --git a/prometheus/push/push.go b/prometheus/push/push.go index e524aa130..56918eae4 100644 --- a/prometheus/push/push.go +++ b/prometheus/push/push.go @@ -70,8 +70,9 @@ type HTTPDoer interface { type Pusher struct { error error - url, job string - grouping map[string]string + url, job string + grouping map[string]string + validationScheme model.ValidationScheme gatherers prometheus.Gatherers registerer prometheus.Registerer @@ -84,11 +85,7 @@ type Pusher struct { expfmt expfmt.Format } -// New creates a new Pusher to push to the provided URL with the provided job -// name (which must not be empty). You can use just host:port or ip:port as url, -// in which case “http://” is added automatically. Alternatively, include the -// schema in the URL. However, do not include the “/metrics/jobs/…” part. -func New(url, job string) *Pusher { +func newPusher(url, job string, validationScheme model.ValidationScheme) *Pusher { var ( reg = prometheus.NewRegistry() err error @@ -102,14 +99,15 @@ func New(url, job string) *Pusher { url = strings.TrimSuffix(url, "/") return &Pusher{ - error: err, - url: url, - job: job, - grouping: map[string]string{}, - gatherers: prometheus.Gatherers{reg}, - registerer: reg, - client: &http.Client{}, - expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim), + error: err, + url: url, + job: job, + grouping: map[string]string{}, + validationScheme: validationScheme, + gatherers: newGatherers([]prometheus.Gatherer{reg}, validationScheme), + registerer: reg, + client: &http.Client{}, + expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim), } } @@ -147,16 +145,6 @@ func (p *Pusher) AddContext(ctx context.Context) error { return p.push(ctx, http.MethodPost) } -// Gatherer adds a Gatherer to the Pusher, from which metrics will be gathered -// to push them to the Pushgateway. The gathered metrics must not contain a job -// label of their own. -// -// For convenience, this method returns a pointer to the Pusher itself. -func (p *Pusher) Gatherer(g prometheus.Gatherer) *Pusher { - p.gatherers = append(p.gatherers, g) - return p -} - // Collector adds a Collector to the Pusher, from which metrics will be // collected to push them to the Pushgateway. The collected metrics must not // contain a job label of their own. @@ -182,7 +170,7 @@ func (p *Pusher) Error() error { // For convenience, this method returns a pointer to the Pusher itself. func (p *Pusher) Grouping(name, value string) *Pusher { if p.error == nil { - if !model.LabelName(name).IsValid() { + if !isLabelNameValid(name, p.validationScheme) { p.error = fmt.Errorf("grouping label has invalid name: %s", name) return p } diff --git a/prometheus/push/push_globalvalidationscheme.go b/prometheus/push/push_globalvalidationscheme.go new file mode 100644 index 000000000..7dc20423b --- /dev/null +++ b/prometheus/push/push_globalvalidationscheme.go @@ -0,0 +1,49 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package push + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +// New creates a new Pusher to push to the provided URL with the provided job +// name (which must not be empty). You can use just host:port or ip:port as url, +// in which case “http://” is added automatically. Alternatively, include the +// schema in the URL. However, do not include the “/metrics/jobs/…” part. +func New(url, job string) *Pusher { + //nolint:staticcheck // model.NameValidationScheme is being phased out. + return newPusher(url, job, model.NameValidationScheme) +} + +func isLabelNameValid(labelName string, _ model.ValidationScheme) bool { + return model.LabelName(labelName).IsValid() +} + +func newGatherers(gatherers []prometheus.Gatherer, _ model.ValidationScheme) prometheus.Gatherers { + return prometheus.NewGatherers(gatherers) +} + +// Gatherer adds a Gatherer to the Pusher, from which metrics will be gathered +// to push them to the Pushgateway. The gathered metrics must not contain a job +// label of their own. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Gatherer(g prometheus.Gatherer) *Pusher { + p.gatherers = append(p.gatherers, g) + return p +} diff --git a/prometheus/push/push_localvalidationscheme.go b/prometheus/push/push_localvalidationscheme.go new file mode 100644 index 000000000..aadf03e9d --- /dev/null +++ b/prometheus/push/push_localvalidationscheme.go @@ -0,0 +1,60 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package push + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +// Option used to create Pusher instances. +type Option struct { + NameValidationScheme model.ValidationScheme +} + +// New creates a new Pusher to push to the provided URL with the provided job +// name (which must not be empty). You can use just host:port or ip:port as url, +// in which case “http://” is added automatically. Alternatively, include the +// schema in the URL. However, do not include the “/metrics/jobs/…” part. +func New(url, job string, opts ...Option) *Pusher { + validationScheme := model.UTF8Validation + for _, opt := range opts { + if opt.NameValidationScheme != model.UnsetValidation { + validationScheme = opt.NameValidationScheme + } + } + + return newPusher(url, job, validationScheme) +} + +func isLabelNameValid(labelName string, scheme model.ValidationScheme) bool { + return model.LabelName(labelName).IsValid(scheme) +} + +func newGatherers(gatherers []prometheus.Gatherer, scheme model.ValidationScheme) prometheus.Gatherers { + return prometheus.NewGatherers(gatherers, scheme) +} + +// Gatherer adds a Gatherer to the Pusher, from which metrics will be gathered +// to push them to the Pushgateway. The gathered metrics must not contain a job +// label of their own. +// +// For convenience, this method returns a pointer to the Pusher itself. +func (p *Pusher) Gatherer(g prometheus.Gatherer) *Pusher { + p.gatherers.AddGatherer(g) + return p +} diff --git a/prometheus/registry.go b/prometheus/registry.go index c6fd2f58b..571b72636 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -31,6 +31,7 @@ import ( "github.com/cespare/xxhash/v2" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" ) @@ -62,32 +63,6 @@ func init() { MustRegister(NewGoCollector()) } -// NewRegistry creates a new vanilla Registry without any Collectors -// pre-registered. -func NewRegistry() *Registry { - return &Registry{ - collectorsByID: map[uint64]Collector{}, - descIDs: map[uint64]struct{}{}, - dimHashesByName: map[string]uint64{}, - } -} - -// NewPedanticRegistry returns a registry that checks during collection if each -// collected Metric is consistent with its reported Desc, and if the Desc has -// actually been registered with the registry. Unchecked Collectors (those whose -// Describe method does not yield any descriptors) are excluded from the check. -// -// Usually, a Registry will be happy as long as the union of all collected -// Metrics is consistent and valid even if some metrics are not consistent with -// their own Desc or a Desc provided by their registered Collector. Well-behaved -// Collectors and Metrics will only provide consistent Descs. This Registry is -// useful to test the implementation of Collectors and Metrics. -func NewPedanticRegistry() *Registry { - r := NewRegistry() - r.pedanticChecksEnabled = true - return r -} - // Registerer is the interface for the part of a registry in charge of // registering and unregistering. Users of custom registries should use // Registerer as type for registration purposes (rather than the Registry type @@ -264,6 +239,7 @@ type Registry struct { dimHashesByName map[string]uint64 uncheckedCollectors []Collector pedanticChecksEnabled bool + nameValidationScheme model.ValidationScheme } // Register implements Registerer. @@ -503,6 +479,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, registeredDescIDs, + r.nameValidationScheme, )) case metric, ok := <-umc: if !ok { @@ -513,6 +490,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, nil, + r.nameValidationScheme, )) default: if goroutineBudget <= 0 || len(checkedCollectors)+len(uncheckedCollectors) == 0 { @@ -530,6 +508,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, registeredDescIDs, + r.nameValidationScheme, )) case metric, ok := <-umc: if !ok { @@ -540,6 +519,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, nil, + r.nameValidationScheme, )) } break @@ -622,6 +602,7 @@ func processMetric( metricFamiliesByName map[string]*dto.MetricFamily, metricHashes map[uint64]struct{}, registeredDescIDs map[uint64]struct{}, + nameValidationScheme model.ValidationScheme, ) error { desc := metric.Desc() // Wrapped metrics collected by an unchecked Collector can have an @@ -705,7 +686,7 @@ func processMetric( } metricFamiliesByName[desc.fqName] = metricFamily } - if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes); err != nil { + if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes, nameValidationScheme); err != nil { return err } if registeredDescIDs != nil { @@ -724,33 +705,15 @@ func processMetric( return nil } -// Gatherers is a slice of Gatherer instances that implements the Gatherer -// interface itself. Its Gather method calls Gather on all Gatherers in the -// slice in order and returns the merged results. Errors returned from the -// Gather calls are all returned in a flattened MultiError. Duplicate and -// inconsistent Metrics are skipped (first occurrence in slice order wins) and -// reported in the returned error. -// -// Gatherers can be used to merge the Gather results from multiple -// Registries. It also provides a way to directly inject existing MetricFamily -// protobufs into the gathering by creating a custom Gatherer with a Gather -// method that simply returns the existing MetricFamily protobufs. Note that no -// registration is involved (in contrast to Collector registration), so -// obviously registration-time checks cannot happen. Any inconsistencies between -// the gathered MetricFamilies are reported as errors by the Gather method, and -// inconsistent Metrics are dropped. Invalid parts of the MetricFamilies -// (e.g. syntactically invalid metric or label names) will go undetected. -type Gatherers []Gatherer - // Gather implements Gatherer. -func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { +func (gs Gatherers) gather(gatherers []Gatherer, scheme model.ValidationScheme) ([]*dto.MetricFamily, error) { var ( metricFamiliesByName = map[string]*dto.MetricFamily{} metricHashes = map[uint64]struct{}{} errs MultiError // The collected errors to return in the end. ) - for i, g := range gs { + for i, g := range gatherers { mfs, err := g.Gather() if err != nil { multiErr := MultiError{} @@ -791,7 +754,7 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { metricFamiliesByName[mf.GetName()] = existingMF } for _, m := range mf.Metric { - if err := checkMetricConsistency(existingMF, m, metricHashes); err != nil { + if err := checkMetricConsistency(existingMF, m, metricHashes, scheme); err != nil { errs = append(errs, err) continue } @@ -870,6 +833,7 @@ func checkMetricConsistency( metricFamily *dto.MetricFamily, dtoMetric *dto.Metric, metricHashes map[uint64]struct{}, + nameValidationScheme model.ValidationScheme, ) error { name := metricFamily.GetName() @@ -894,7 +858,7 @@ func checkMetricConsistency( name, dtoMetric, labelName, ) } - if !checkLabelName(labelName) { + if !checkLabelName(labelName, nameValidationScheme) { return fmt.Errorf( "collected metric %q { %s} has a label with an invalid name: %s", name, dtoMetric, labelName, diff --git a/prometheus/registry_globalvalidationscheme.go b/prometheus/registry_globalvalidationscheme.go new file mode 100644 index 000000000..b7ecb5371 --- /dev/null +++ b/prometheus/registry_globalvalidationscheme.go @@ -0,0 +1,78 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus + +import ( + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" +) + +// NewRegistry creates a new vanilla Registry without any Collectors +// pre-registered. +func NewRegistry() *Registry { + return &Registry{ + collectorsByID: map[uint64]Collector{}, + descIDs: map[uint64]struct{}{}, + dimHashesByName: map[string]uint64{}, + //nolint:staticcheck // model.NameValidationScheme is being phased out. + nameValidationScheme: model.NameValidationScheme, + } +} + +// NewPedanticRegistry returns a registry that checks during collection if each +// collected Metric is consistent with its reported Desc, and if the Desc has +// actually been registered with the registry. Unchecked Collectors (those whose +// Describe method does not yield any descriptors) are excluded from the check. +// +// Usually, a Registry will be happy as long as the union of all collected +// Metrics is consistent and valid even if some metrics are not consistent with +// their own Desc or a Desc provided by their registered Collector. Well-behaved +// Collectors and Metrics will only provide consistent Descs. This Registry is +// useful to test the implementation of Collectors and Metrics. +func NewPedanticRegistry() *Registry { + r := NewRegistry() + r.pedanticChecksEnabled = true + return r +} + +// Gatherers is a slice of Gatherer instances that implements the Gatherer +// interface itself. Its Gather method calls Gather on all Gatherers in the +// slice in order and returns the merged results. Errors returned from the +// Gather calls are all returned in a flattened MultiError. Duplicate and +// inconsistent Metrics are skipped (first occurrence in slice order wins) and +// reported in the returned error. +// +// Gatherers can be used to merge the Gather results from multiple +// Registries. It also provides a way to directly inject existing MetricFamily +// protobufs into the gathering by creating a custom Gatherer with a Gather +// method that simply returns the existing MetricFamily protobufs. Note that no +// registration is involved (in contrast to Collector registration), so +// obviously registration-time checks cannot happen. Any inconsistencies between +// the gathered MetricFamilies are reported as errors by the Gather method, and +// inconsistent Metrics are dropped. Invalid parts of the MetricFamilies +// (e.g. syntactically invalid metric or label names) will go undetected. +type Gatherers []Gatherer + +// NewGatherers returns a new Gatherers encapsulating the provided gatherers. +func NewGatherers(gatherers []Gatherer) Gatherers { + return Gatherers(gatherers) +} + +// Gather implements Gatherer. +func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { + //nolint:staticcheck // model.NameValidationScheme is being phased out. + return gs.gather(gs, model.NameValidationScheme) +} diff --git a/prometheus/registry_globalvalidationscheme_test.go b/prometheus/registry_globalvalidationscheme_test.go new file mode 100644 index 000000000..45a16147c --- /dev/null +++ b/prometheus/registry_globalvalidationscheme_test.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !localvalidationscheme + +package prometheus_test + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func newGatherers(gatherers []prometheus.Gatherer, _ model.ValidationScheme) prometheus.Gatherers { + return prometheus.NewGatherers(gatherers) +} diff --git a/prometheus/registry_localvalidationscheme.go b/prometheus/registry_localvalidationscheme.go new file mode 100644 index 000000000..f85b09810 --- /dev/null +++ b/prometheus/registry_localvalidationscheme.go @@ -0,0 +1,103 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus + +import ( + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" +) + +type RegistryOption struct { + NameValidationScheme model.ValidationScheme +} + +// NewRegistry creates a new vanilla Registry without any Collectors +// pre-registered. +func NewRegistry(opts ...RegistryOption) *Registry { + scheme := model.UTF8Validation + for _, opt := range opts { + if opt.NameValidationScheme != model.UnsetValidation { + scheme = opt.NameValidationScheme + } + } + return &Registry{ + collectorsByID: map[uint64]Collector{}, + descIDs: map[uint64]struct{}{}, + dimHashesByName: map[string]uint64{}, + nameValidationScheme: scheme, + } +} + +// NewPedanticRegistry returns a registry that checks during collection if each +// collected Metric is consistent with its reported Desc, and if the Desc has +// actually been registered with the registry. Unchecked Collectors (those whose +// Describe method does not yield any descriptors) are excluded from the check. +// +// Usually, a Registry will be happy as long as the union of all collected +// Metrics is consistent and valid even if some metrics are not consistent with +// their own Desc or a Desc provided by their registered Collector. Well-behaved +// Collectors and Metrics will only provide consistent Descs. This Registry is +// useful to test the implementation of Collectors and Metrics. +func NewPedanticRegistry(opts ...RegistryOption) *Registry { + r := NewRegistry(opts...) + r.pedanticChecksEnabled = true + return r +} + +// Gatherers is a collection of Gatherer instances that implements the Gatherer +// interface itself. Its Gather method calls Gather on all Gatherers in the +// slice in order and returns the merged results. Errors returned from the +// Gather calls are all returned in a flattened MultiError. Duplicate and +// inconsistent Metrics are skipped (first occurrence in slice order wins) and +// reported in the returned error. +// +// Gatherers can be used to merge the Gather results from multiple +// Registries. It also provides a way to directly inject existing MetricFamily +// protobufs into the gathering by creating a custom Gatherer with a Gather +// method that simply returns the existing MetricFamily protobufs. Note that no +// registration is involved (in contrast to Collector registration), so +// obviously registration-time checks cannot happen. Any inconsistencies between +// the gathered MetricFamilies are reported as errors by the Gather method, and +// inconsistent Metrics are dropped. Invalid parts of the MetricFamilies +// (e.g. syntactically invalid metric or label names) will go undetected. +type Gatherers struct { + gatherers []Gatherer + validationScheme model.ValidationScheme +} + +// NewGatherers returns a new Gatherers encapsulating the provided gatherers. +func NewGatherers(gatherers []Gatherer, validationScheme model.ValidationScheme) Gatherers { + return Gatherers{ + gatherers: gatherers, + validationScheme: validationScheme, + } +} + +// Gather implements Gatherer. +func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { + return gs.gather(gs.gatherers, gs.validationScheme) +} + +func (gs *Gatherers) AddGatherer(g Gatherer) { + gs.gatherers = append(gs.gatherers, g) +} + +// Range calls f for every Gatherer in gs. +func (gs *Gatherers) Range(f func(Gatherer)) { + for _, g := range gs.gatherers { + f(g) + } +} diff --git a/prometheus/registry_localvalidationscheme_test.go b/prometheus/registry_localvalidationscheme_test.go new file mode 100644 index 000000000..2267dbb82 --- /dev/null +++ b/prometheus/registry_localvalidationscheme_test.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build localvalidationscheme + +package prometheus_test + +import ( + "github.com/prometheus/common/model" + + "github.com/prometheus/client_golang/prometheus" +) + +func newGatherers(gatherers []prometheus.Gatherer, scheme model.ValidationScheme) prometheus.Gatherers { + return prometheus.NewGatherers(gatherers, scheme) +} diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index 12b09d623..4be97913f 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -37,6 +37,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" "go.uber.org/goleak" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -702,12 +703,12 @@ collected metric "broken_metric" { label: label: