diff --git a/.circleci/config.yml b/.circleci/config.yml index 95fb827c..c9224cdf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,6 +12,9 @@ jobs: use_gomod_cache: type: boolean default: true + build_tags: + type: string + default: "" docker: - image: cimg/go:<< parameters.go_version >> environment: @@ -24,7 +27,12 @@ jobs: steps: - go/load-cache: key: v1-go<< parameters.go_version >> - - run: make test + - run: | + if [ -n "<< parameters.build_tags >>" ]; then + make test GOOPTS="-tags=<< parameters.build_tags >>" + else + make test + fi - when: condition: << parameters.use_gomod_cache >> steps: @@ -89,12 +97,15 @@ workflows: jobs: # Supported Go versions are synced with github.com/prometheus/client_golang. - test: - name: go-<< matrix.go_version >> + name: go-<< matrix.go_version >><<# matrix.build_tags >>-<< matrix.build_tags >><> matrix: parameters: go_version: - "1.23" - "1.24" + build_tags: + - "" + - "localvalidationscheme" - test-assets: name: assets-go-<< matrix.go_version >> matrix: diff --git a/README.md b/README.md index f7d5342d..5001a34c 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,17 @@ any stability guarantees for external usage. * **route**: A routing wrapper around [httprouter](https://github.com/julienschmidt/httprouter) using `context.Context` * **server**: Common servers * **version**: Version information and metrics + +## Metric/label name validation scheme + +The libraries in this Go module share a notion of metric and label name validation scheme. +There are two different schemes to choose from: +* `model.LegacyValidation` => Metric and label names have to conform to the original Prometheus character requirements +* `model.UTF8Validation` => Metric and label names are only required to be valid UTF-8 strings + +The active name validation scheme is normally implicitly controlled via the global variable `model.NameValidationScheme`. +It's used by functions such as `model.IsValidMetricName` and `model.LabelName.IsValid`. +_However_, if building with the _experimental_ build tag `localvalidationscheme`, the `model.NameValidationScheme` global is removed, and the API changes to accept the name validation scheme as an explicit parameter. +`model.NameValidationScheme` is deprecated, and at some point, the API currently controlled by the build tag `localvalidationscheme` becomes standard. +All users of this library will be expected to move to the new API, and there will be an announcement in the changelog when the global is removed. +For the time being, the `localvalidationscheme` build tag is experimental and the API enabled by it may change. diff --git a/expfmt/decode.go b/expfmt/decode.go index 1448439b..8e1b02f0 100644 --- a/expfmt/decode.go +++ b/expfmt/decode.go @@ -14,7 +14,6 @@ package expfmt import ( - "bufio" "fmt" "io" "math" @@ -70,21 +69,6 @@ func ResponseFormat(h http.Header) Format { return FmtUnknown } -// NewDecoder returns a new decoder based on the given input format. -// If the input format does not imply otherwise, a text format decoder is returned. -func NewDecoder(r io.Reader, format Format) Decoder { - switch format.FormatType() { - case TypeProtoDelim: - return &protoDecoder{r: bufio.NewReader(r)} - } - return &textDecoder{r: r} -} - -// protoDecoder implements the Decoder interface for protocol buffers. -type protoDecoder struct { - r protodelim.Reader -} - // Decode implements the Decoder interface. func (d *protoDecoder) Decode(v *dto.MetricFamily) error { opts := protodelim.UnmarshalOptions{ @@ -93,7 +77,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error { if err := opts.UnmarshalFrom(d.r, v); err != nil { return err } - if !model.IsValidMetricName(model.LabelValue(v.GetName())) { + if !d.isValidMetricName(v.GetName()) { return fmt.Errorf("invalid metric name %q", v.GetName()) } for _, m := range v.GetMetric() { @@ -107,7 +91,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error { if !model.LabelValue(l.GetValue()).IsValid() { return fmt.Errorf("invalid label value %q", l.GetValue()) } - if !model.LabelName(l.GetName()).IsValid() { + if !d.isValidLabelName(l.GetName()) { return fmt.Errorf("invalid label name %q", l.GetName()) } } diff --git a/expfmt/decode_globalvalidationscheme.go b/expfmt/decode_globalvalidationscheme.go new file mode 100644 index 00000000..ffb8b6ed --- /dev/null +++ b/expfmt/decode_globalvalidationscheme.go @@ -0,0 +1,48 @@ +// 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 expfmt + +import ( + "bufio" + "io" + + "google.golang.org/protobuf/encoding/protodelim" + + "github.com/prometheus/common/model" +) + +// protoDecoder implements the Decoder interface for protocol buffers. +type protoDecoder struct { + r protodelim.Reader +} + +// NewDecoder returns a new decoder based on the given input format. +// If the input format does not imply otherwise, a text format decoder is returned. +func NewDecoder(r io.Reader, format Format) Decoder { + switch format.FormatType() { + case TypeProtoDelim: + return &protoDecoder{r: bufio.NewReader(r)} + } + return &textDecoder{r: r} +} + +func (d *protoDecoder) isValidMetricName(name string) bool { + return model.IsValidMetricName(model.LabelValue(name)) +} + +func (d *protoDecoder) isValidLabelName(name string) bool { + return model.LabelName(name).IsValid() +} diff --git a/expfmt/decode_globalvalidationscheme_test.go b/expfmt/decode_globalvalidationscheme_test.go new file mode 100644 index 00000000..d16c9e56 --- /dev/null +++ b/expfmt/decode_globalvalidationscheme_test.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 expfmt + +import ( + "io" + "testing" + + "github.com/prometheus/common/model" +) + +func newDecoder(t *testing.T, r io.Reader, format Format, scheme model.ValidationScheme) Decoder { + //nolint:staticcheck // NameValidationScheme is being phased out. + origScheme := model.NameValidationScheme + t.Cleanup(func() { + //nolint:staticcheck // NameValidationScheme is being phased out. + model.NameValidationScheme = origScheme + }) + //nolint:staticcheck // NameValidationScheme is being phased out. + model.NameValidationScheme = scheme + + return NewDecoder(r, format) +} diff --git a/expfmt/decode_localvalidationscheme.go b/expfmt/decode_localvalidationscheme.go new file mode 100644 index 00000000..8e0dbb59 --- /dev/null +++ b/expfmt/decode_localvalidationscheme.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 expfmt + +import ( + "bufio" + "io" + + "google.golang.org/protobuf/encoding/protodelim" + + "github.com/prometheus/common/model" +) + +// protoDecoder implements the Decoder interface for protocol buffers. +type protoDecoder struct { + r protodelim.Reader + validationScheme model.ValidationScheme +} + +// NewDecoder returns a new decoder based on the given input format. +// If the input format does not imply otherwise, a text format decoder is returned. +func NewDecoder(r io.Reader, format Format, validationScheme model.ValidationScheme) Decoder { + switch format.FormatType() { + case TypeProtoDelim: + return &protoDecoder{ + r: bufio.NewReader(r), + validationScheme: validationScheme, + } + } + return &textDecoder{r: r} +} + +func (d *protoDecoder) isValidMetricName(name string) bool { + return model.IsValidMetricName(model.LabelValue(name), d.validationScheme) +} + +func (d *protoDecoder) isValidLabelName(name string) bool { + return model.LabelName(name).IsValid(d.validationScheme) +} diff --git a/expfmt/decode_localvalidationscheme_test.go b/expfmt/decode_localvalidationscheme_test.go new file mode 100644 index 00000000..65bd2637 --- /dev/null +++ b/expfmt/decode_localvalidationscheme_test.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 expfmt + +import ( + "io" + "testing" + + "github.com/prometheus/common/model" +) + +func newDecoder(_ *testing.T, r io.Reader, format Format, scheme model.ValidationScheme) Decoder { + return NewDecoder(r, format, scheme) +} diff --git a/expfmt/decode_test.go b/expfmt/decode_test.go index 759ff746..1374bc83 100644 --- a/expfmt/decode_test.go +++ b/expfmt/decode_test.go @@ -23,6 +23,7 @@ import ( "os" "reflect" "sort" + "strconv" "strings" "testing" @@ -360,46 +361,46 @@ func TestProtoDecoder(t *testing.T) { } for i, scenario := range scenarios { - dec := &SampleDecoder{ - Dec: &protoDecoder{r: strings.NewReader(scenario.in)}, - Opts: &DecodeOptions{ - Timestamp: testTime, - }, - } - - var all model.Vector - for { - model.NameValidationScheme = model.LegacyValidation //nolint:staticcheck - var smpls model.Vector - err := dec.Decode(&smpls) - if err != nil && errors.Is(err, io.EOF) { - break + t.Run(strconv.Itoa(i), func(t *testing.T) { + dec := &SampleDecoder{ + Dec: newDecoder(t, strings.NewReader(scenario.in), FmtProtoDelim, model.LegacyValidation), + Opts: &DecodeOptions{ + Timestamp: testTime, + }, } - if scenario.legacyNameFail { - require.Errorf(t, err, "Expected error when decoding without UTF-8 support enabled but got none") - model.NameValidationScheme = model.UTF8Validation //nolint:staticcheck - dec = &SampleDecoder{ - Dec: &protoDecoder{r: strings.NewReader(scenario.in)}, - Opts: &DecodeOptions{ - Timestamp: testTime, - }, + + var all model.Vector + for { + var smpls model.Vector + err := dec.Decode(&smpls) + if err != nil && errors.Is(err, io.EOF) { + break + } + if scenario.legacyNameFail { + require.Errorf(t, err, "Expected error when decoding without UTF-8 support enabled but got none") + dec = &SampleDecoder{ + Dec: newDecoder(t, strings.NewReader(scenario.in), FmtProtoDelim, model.UTF8Validation), + Opts: &DecodeOptions{ + Timestamp: testTime, + }, + } + err = dec.Decode(&smpls) + if errors.Is(err, io.EOF) { + break + } + require.NoErrorf(t, err, "Unexpected error when decoding with UTF-8 support: %v", err) } - err = dec.Decode(&smpls) - if errors.Is(err, io.EOF) { + if scenario.fail { + require.Errorf(t, err, "Expected error but got none") break } - require.NoErrorf(t, err, "Unexpected error when decoding with UTF-8 support: %v", err) + require.NoError(t, err) + all = append(all, smpls...) } - if scenario.fail { - require.Errorf(t, err, "Expected error but got none") - break - } - require.NoError(t, err) - all = append(all, smpls...) - } - sort.Sort(all) - sort.Sort(scenario.expected) - require.Truef(t, reflect.DeepEqual(all, scenario.expected), "%d. output does not match, want: %#v, got %#v", i, scenario.expected, all) + sort.Sort(all) + sort.Sort(scenario.expected) + require.Truef(t, reflect.DeepEqual(all, scenario.expected), "%d. output does not match, want: %#v, got %#v", i, scenario.expected, all) + }) } } @@ -408,7 +409,7 @@ func TestProtoMultiMessageDecoder(t *testing.T) { require.NoErrorf(t, err, "Reading file failed: %v", err) buf := bytes.NewReader(data) - decoder := NewDecoder(buf, FmtProtoDelim) + decoder := newDecoder(t, buf, FmtProtoDelim, model.UTF8Validation) var metrics []*dto.MetricFamily for { var mf dto.MetricFamily @@ -557,7 +558,7 @@ func TestTextDecoderWithBufioReader(t *testing.T) { var decoded bool r := bufio.NewReader(strings.NewReader(example)) - dec := NewDecoder(r, FmtText) + dec := newDecoder(t, r, FmtText, model.UTF8Validation) for { var mf dto.MetricFamily if err := dec.Decode(&mf); err != nil { diff --git a/go.mod b/go.mod index 1381a9bb..d42cad20 100644 --- a/go.mod +++ b/go.mod @@ -25,8 +25,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.20.4 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.22.1-0.20250714095417-1649bc88444a // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 3c89e304..4ca937b2 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,6 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -29,12 +27,12 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= -github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.22.1-0.20250714095417-1649bc88444a h1:GEfwJBcLyfuBrPfsux6paIAgUGalKf+EESvGZRE0jk0= +github.com/prometheus/client_golang v1.22.1-0.20250714095417-1649bc88444a/go.mod h1:CR59Lx0qSGs4e5Zxr3VgiN2yuxy90eRO/0sO/iCjUH0= 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/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -43,6 +41,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +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.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= diff --git a/model/alert.go b/model/alert.go index 460f554f..2cb61932 100644 --- a/model/alert.go +++ b/model/alert.go @@ -88,20 +88,20 @@ func (a *Alert) StatusAt(ts time.Time) AlertStatus { } // Validate checks whether the alert data is inconsistent. -func (a *Alert) Validate() error { +func (a *Alert) validate(scheme ValidationScheme) error { if a.StartsAt.IsZero() { return errors.New("start time missing") } if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) { return errors.New("start time must be before end time") } - if err := a.Labels.Validate(); err != nil { + if err := a.Labels.validate(scheme); err != nil { return fmt.Errorf("invalid label set: %w", err) } if len(a.Labels) == 0 { return errors.New("at least one label pair required") } - if err := a.Annotations.Validate(); err != nil { + if err := a.Annotations.validate(scheme); err != nil { return fmt.Errorf("invalid annotations: %w", err) } return nil diff --git a/model/alert_globalvalidationscheme.go b/model/alert_globalvalidationscheme.go new file mode 100644 index 00000000..b71b84eb --- /dev/null +++ b/model/alert_globalvalidationscheme.go @@ -0,0 +1,21 @@ +// 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 model + +// Validate checks whether the alert data is inconsistent. +func (a *Alert) Validate() error { + return a.validate(NameValidationScheme) +} diff --git a/model/alert_globalvalidationscheme_test.go b/model/alert_globalvalidationscheme_test.go new file mode 100644 index 00000000..d6716346 --- /dev/null +++ b/model/alert_globalvalidationscheme_test.go @@ -0,0 +1,29 @@ +// 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 model + +import ( + "testing" +) + +func testValidate(t *testing.T, alert *Alert, scheme ValidationScheme) error { + origScheme := NameValidationScheme + t.Cleanup(func() { + NameValidationScheme = origScheme + }) + NameValidationScheme = scheme + return alert.Validate() +} diff --git a/model/alert_localvalidationscheme.go b/model/alert_localvalidationscheme.go new file mode 100644 index 00000000..eb3e7ff7 --- /dev/null +++ b/model/alert_localvalidationscheme.go @@ -0,0 +1,21 @@ +// 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 model + +// Validate checks whether the alert data is inconsistent. +func (a *Alert) Validate(scheme ValidationScheme) error { + return a.validate(scheme) +} diff --git a/model/alert_localvalidationscheme_test.go b/model/alert_localvalidationscheme_test.go new file mode 100644 index 00000000..0a9f5907 --- /dev/null +++ b/model/alert_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 model + +import "testing" + +func testValidate(_ *testing.T, alert *Alert, scheme ValidationScheme) error { + return alert.Validate(scheme) +} diff --git a/model/alert_test.go b/model/alert_test.go index fc3eaf10..cc0f2fa5 100644 --- a/model/alert_test.go +++ b/model/alert_test.go @@ -16,17 +16,13 @@ package model import ( "fmt" "sort" + "strconv" "strings" "testing" "time" ) func TestAlertValidate(t *testing.T) { - oldScheme := NameValidationScheme - NameValidationScheme = LegacyValidation - defer func() { - NameValidationScheme = oldScheme - }() ts := time.Now() cases := []struct { @@ -106,21 +102,23 @@ func TestAlertValidate(t *testing.T) { } for i, c := range cases { - err := c.alert.Validate() - if err == nil { + t.Run(strconv.Itoa(i), func(t *testing.T) { + err := testValidate(t, c.alert, LegacyValidation) + if err == nil { + if c.err == "" { + return + } + t.Errorf("%d. Expected error %q but got none", i, c.err) + return + } if c.err == "" { - continue + t.Errorf("%d. Expected no error but got %q", i, err) + return } - t.Errorf("%d. Expected error %q but got none", i, c.err) - continue - } - if c.err == "" { - t.Errorf("%d. Expected no error but got %q", i, err) - continue - } - if !strings.Contains(err.Error(), c.err) { - t.Errorf("%d. Expected error to contain %q but got %q", i, c.err, err) - } + if !strings.Contains(err.Error(), c.err) { + t.Errorf("%d. Expected error to contain %q but got %q", i, c.err, err) + } + }) } } diff --git a/model/labels.go b/model/labels.go index e2ff8359..112d61c9 100644 --- a/model/labels.go +++ b/model/labels.go @@ -19,6 +19,8 @@ import ( "regexp" "strings" "unicode/utf8" + + "gopkg.in/yaml.v2" ) const ( @@ -103,20 +105,17 @@ var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") // therewith. type LabelName string -// IsValid returns true iff the name matches the pattern of LabelNameRE when -// NameValidationScheme is set to LegacyValidation, or valid UTF-8 if -// NameValidationScheme is set to UTF8Validation. -func (ln LabelName) IsValid() bool { +func (ln LabelName) isValid(scheme ValidationScheme) bool { if len(ln) == 0 { return false } - switch NameValidationScheme { + switch scheme { case LegacyValidation: return ln.IsValidLegacy() case UTF8Validation: return utf8.ValidString(string(ln)) default: - panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme)) + panic(fmt.Sprintf("Invalid name validation scheme requested: %s", scheme)) } } @@ -136,31 +135,11 @@ func (ln LabelName) IsValidLegacy() bool { return true } -// UnmarshalYAML implements the yaml.Unmarshaler interface. -func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { - var s string - if err := unmarshal(&s); err != nil { - return err - } - if !LabelName(s).IsValid() { - return fmt.Errorf("%q is not a valid label name", s) - } - *ln = LabelName(s) - return nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (ln *LabelName) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } - if !LabelName(s).IsValid() { - return fmt.Errorf("%q is not a valid label name", s) - } - *ln = LabelName(s) - return nil -} +var ( + labelName LabelName + _ yaml.Unmarshaler = &labelName + _ json.Unmarshaler = &labelName +) // LabelNames is a sortable LabelName slice. In implements sort.Interface. type LabelNames []LabelName diff --git a/model/labels_globalvalidationscheme.go b/model/labels_globalvalidationscheme.go new file mode 100644 index 00000000..0460a4d3 --- /dev/null +++ b/model/labels_globalvalidationscheme.go @@ -0,0 +1,53 @@ +// 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 model + +import ( + "encoding/json" + "fmt" +) + +// IsValid returns true iff the name matches the pattern of LabelNameRE when +// scheme is LegacyValidation, or valid UTF-8 if it is UTF8Validation. +func (ln LabelName) IsValid() bool { + return ln.isValid(NameValidationScheme) +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + if !LabelName(s).IsValid() { + return fmt.Errorf("%q is not a valid label name", s) + } + *ln = LabelName(s) + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (ln *LabelName) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + if !LabelName(s).IsValid() { + return fmt.Errorf("%q is not a valid label name", s) + } + *ln = LabelName(s) + return nil +} diff --git a/model/labels_globalvalidationscheme_test.go b/model/labels_globalvalidationscheme_test.go new file mode 100644 index 00000000..17a189a3 --- /dev/null +++ b/model/labels_globalvalidationscheme_test.go @@ -0,0 +1,39 @@ +// 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 model + +import "testing" + +func testLabelNameIsValid(t *testing.T, labelName LabelName, legacyValid, utf8Valid bool) { + t.Helper() + + origScheme := NameValidationScheme + t.Cleanup(func() { + NameValidationScheme = origScheme + }) + + NameValidationScheme = LegacyValidation + if labelName.IsValid() != legacyValid { + t.Errorf("Expected %v for %q using legacy IsValid method", legacyValid, labelName) + } + if LabelNameRE.MatchString(string(labelName)) != legacyValid { + t.Errorf("Expected %v for %q using legacy regexp match", legacyValid, labelName) + } + NameValidationScheme = UTF8Validation + if labelName.IsValid() != utf8Valid { + t.Errorf("Expected %v for %q using UTF-8 IsValid method", legacyValid, labelName) + } +} diff --git a/model/labels_localvalidationscheme.go b/model/labels_localvalidationscheme.go new file mode 100644 index 00000000..9b7e4aab --- /dev/null +++ b/model/labels_localvalidationscheme.go @@ -0,0 +1,55 @@ +// 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 model + +import ( + "encoding/json" + "fmt" +) + +// IsValid returns true iff the name matches the pattern of LabelNameRE when +// scheme is LegacyValidation, or valid UTF-8 if it is UTF8Validation. +func (ln LabelName) IsValid(validationScheme ValidationScheme) bool { + return ln.isValid(validationScheme) +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +// Validation is done using UTF8Validation. +func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + if !LabelName(s).IsValid(UTF8Validation) { + return fmt.Errorf("%q is not a valid label name", s) + } + *ln = LabelName(s) + return nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// Validation is done using UTF8Validation. +func (ln *LabelName) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + if !LabelName(s).IsValid(UTF8Validation) { + return fmt.Errorf("%q is not a valid label name", s) + } + *ln = LabelName(s) + return nil +} diff --git a/model/labels_localvalidationscheme_test.go b/model/labels_localvalidationscheme_test.go new file mode 100644 index 00000000..a5cff0f1 --- /dev/null +++ b/model/labels_localvalidationscheme_test.go @@ -0,0 +1,32 @@ +// 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 model + +import "testing" + +func testLabelNameIsValid(t *testing.T, labelName LabelName, legacyValid, utf8Valid bool) { + t.Helper() + + if labelName.IsValid(LegacyValidation) != legacyValid { + t.Errorf("Expected %v for %q using legacy IsValid method", legacyValid, labelName) + } + if LabelNameRE.MatchString(string(labelName)) != legacyValid { + t.Errorf("Expected %v for %q using legacy regexp match", legacyValid, labelName) + } + if labelName.IsValid(UTF8Validation) != utf8Valid { + t.Errorf("Expected %v for %q using UTF-8 IsValid method", legacyValid, labelName) + } +} diff --git a/model/labels_test.go b/model/labels_test.go index 23395432..ef8c459a 100644 --- a/model/labels_test.go +++ b/model/labels_test.go @@ -144,17 +144,7 @@ func TestLabelNameIsValid(t *testing.T) { } for _, s := range scenarios { - NameValidationScheme = LegacyValidation - if s.ln.IsValid() != s.legacyValid { - t.Errorf("Expected %v for %q using legacy IsValid method", s.legacyValid, s.ln) - } - if LabelNameRE.MatchString(string(s.ln)) != s.legacyValid { - t.Errorf("Expected %v for %q using legacy regexp match", s.legacyValid, s.ln) - } - NameValidationScheme = UTF8Validation - if s.ln.IsValid() != s.utf8Valid { - t.Errorf("Expected %v for %q using UTF-8 IsValid method", s.legacyValid, s.ln) - } + testLabelNameIsValid(t, s.ln, s.legacyValid, s.utf8Valid) } } diff --git a/model/labelset.go b/model/labelset.go index d0ad88da..f14d2d8a 100644 --- a/model/labelset.go +++ b/model/labelset.go @@ -26,11 +26,9 @@ import ( // match. type LabelSet map[LabelName]LabelValue -// Validate checks whether all names and values in the label set -// are valid. -func (ls LabelSet) Validate() error { +func (ls LabelSet) validate(scheme ValidationScheme) error { for ln, lv := range ls { - if !ln.IsValid() { + if !ln.isValid(scheme) { return fmt.Errorf("invalid name %q", ln) } if !lv.IsValid() { @@ -139,20 +137,7 @@ func (ls LabelSet) FastFingerprint() Fingerprint { return labelSetToFastFingerprint(ls) } -// UnmarshalJSON implements the json.Unmarshaler interface. -func (l *LabelSet) UnmarshalJSON(b []byte) error { - var m map[LabelName]LabelValue - if err := json.Unmarshal(b, &m); err != nil { - return err - } - // encoding/json only unmarshals maps of the form map[string]T. It treats - // LabelName as a string and does not call its UnmarshalJSON method. - // Thus, we have to replicate the behavior here. - for ln := range m { - if !ln.IsValid() { - return fmt.Errorf("%q is not a valid label name", ln) - } - } - *l = LabelSet(m) - return nil -} +var ( + labelSet LabelSet + _ json.Unmarshaler = &labelSet +) diff --git a/model/labelset_globalvalidationscheme.go b/model/labelset_globalvalidationscheme.go new file mode 100644 index 00000000..f978a0d6 --- /dev/null +++ b/model/labelset_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 model + +import ( + "encoding/json" + "fmt" +) + +// Validate checks whether all names and values in the label set +// are valid. +func (ls LabelSet) Validate() error { + return ls.validate(NameValidationScheme) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (l *LabelSet) UnmarshalJSON(b []byte) error { + var m map[LabelName]LabelValue + if err := json.Unmarshal(b, &m); err != nil { + return err + } + // encoding/json only unmarshals maps of the form map[string]T. It treats + // LabelName as a string and does not call its UnmarshalJSON method. + // Thus, we have to replicate the behavior here. + for ln := range m { + if !ln.IsValid() { + return fmt.Errorf("%q is not a valid label name", ln) + } + } + *l = LabelSet(m) + return nil +} diff --git a/model/labelset_localvalidationscheme.go b/model/labelset_localvalidationscheme.go new file mode 100644 index 00000000..5c4c282e --- /dev/null +++ b/model/labelset_localvalidationscheme.go @@ -0,0 +1,46 @@ +// 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 model + +import ( + "encoding/json" + "fmt" +) + +// Validate checks whether all names and values in the label set +// are valid. +func (ls LabelSet) Validate(scheme ValidationScheme) error { + return ls.validate(scheme) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// Validates label names using UTF8Validation. +func (l *LabelSet) UnmarshalJSON(b []byte) error { + var m map[LabelName]LabelValue + if err := json.Unmarshal(b, &m); err != nil { + return err + } + // encoding/json only unmarshals maps of the form map[string]T. It treats + // LabelName as a string and does not call its UnmarshalJSON method. + // Thus, we have to replicate the behavior here. + for ln := range m { + if !ln.IsValid(UTF8Validation) { + return fmt.Errorf("%q is not a valid label name", ln) + } + } + *l = LabelSet(m) + return nil +} diff --git a/model/labelset_test.go b/model/labelset_test.go index 7334b0a0..87d3c0fb 100644 --- a/model/labelset_test.go +++ b/model/labelset_test.go @@ -55,11 +55,13 @@ func TestUnmarshalJSONLabelSet(t *testing.T) { } }` - NameValidationScheme = LegacyValidation - err = json.Unmarshal([]byte(invalidlabelSetJSON), &c) - expectedErr := `"1nvalid_23name" is not a valid label name` + if err := json.Unmarshal([]byte(invalidlabelSetJSON), &c); err != nil { + t.Errorf("unexpected error: %s", err) + } + err = c.LabelSet.validate(LegacyValidation) + const expectedErr = `invalid name "1nvalid_23name"` if err == nil || err.Error() != expectedErr { - t.Errorf("expected an error with message '%s' to be thrown", expectedErr) + t.Errorf("expected an error with message '%s' to be thrown, got: '%s'", expectedErr, err) } } diff --git a/model/metric.go b/model/metric.go index 2bd913ff..abeb31cb 100644 --- a/model/metric.go +++ b/model/metric.go @@ -27,36 +27,13 @@ import ( "gopkg.in/yaml.v2" ) -var ( - // NameValidationScheme determines the global default method of the name - // validation to be used by all calls to IsValidMetricName() and LabelName - // IsValid(). - // - // Deprecated: This variable should not be used and might be removed in the - // far future. If you wish to stick to the legacy name validation use - // `IsValidLegacyMetricName()` and `LabelName.IsValidLegacy()` methods - // instead. This variable is here as an escape hatch for emergency cases, - // given the recent change from `LegacyValidation` to `UTF8Validation`, e.g., - // to delay UTF-8 migrations in time or aid in debugging unforeseen results of - // the change. In such a case, a temporary assignment to `LegacyValidation` - // value in the `init()` function in your main.go or so, could be considered. - // - // Historically we opted for a global variable for feature gating different - // validation schemes in operations that were not otherwise easily adjustable - // (e.g. Labels yaml unmarshaling). That could have been a mistake, a separate - // Labels structure or package might have been a better choice. Given the - // change was made and many upgraded the common already, we live this as-is - // with this warning and learning for the future. - NameValidationScheme = UTF8Validation - - // NameEscapingScheme defines the default way that names will be escaped when - // presented to systems that do not support UTF-8 names. If the Content-Type - // "escaping" term is specified, that will override this value. - // NameEscapingScheme should not be set to the NoEscaping value. That string - // is used in content negotiation to indicate that a system supports UTF-8 and - // has that feature enabled. - NameEscapingScheme = UnderscoreEscaping -) +// NameEscapingScheme defines the default way that names will be escaped when +// presented to systems that do not support UTF-8 names. If the Content-Type +// "escaping" term is specified, that will override this value. +// NameEscapingScheme should not be set to the NoEscaping value. That string +// is used in content negotiation to indicate that a system supports UTF-8 and +// has that feature enabled. +var NameEscapingScheme = UnderscoreEscaping // ValidationScheme is a Go enum for determining how metric and label names will // be validated by this library. @@ -227,11 +204,8 @@ func (m Metric) FastFingerprint() Fingerprint { return LabelSet(m).FastFingerprint() } -// IsValidMetricName returns true iff name matches the pattern of MetricNameRE -// for legacy names, and iff it's valid UTF-8 if the UTF8Validation scheme is -// selected. -func IsValidMetricName(n LabelValue) bool { - switch NameValidationScheme { +func isValidMetricName(n LabelValue, scheme ValidationScheme) bool { + switch scheme { case LegacyValidation: return IsValidLegacyMetricName(string(n)) case UTF8Validation: @@ -240,12 +214,12 @@ func IsValidMetricName(n LabelValue) bool { } return utf8.ValidString(string(n)) default: - panic(fmt.Sprintf("Invalid name validation scheme requested: %s", NameValidationScheme.String())) + panic(fmt.Sprintf("Invalid name validation scheme requested: %s", scheme)) } } // IsValidLegacyMetricName is similar to IsValidMetricName but always uses the -// legacy validation scheme regardless of the value of NameValidationScheme. +// legacy validation scheme. // This function, however, does not use MetricNameRE for the check but a much // faster hardcoded implementation. func IsValidLegacyMetricName(n string) bool { diff --git a/model/metric_globalvalidationscheme.go b/model/metric_globalvalidationscheme.go new file mode 100644 index 00000000..ff038d88 --- /dev/null +++ b/model/metric_globalvalidationscheme.go @@ -0,0 +1,43 @@ +// 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 model + +// NameValidationScheme determines the global default method of the name +// validation to be used by all calls to IsValidMetricName() and LabelName +// IsValid(). +// +// Deprecated: This variable should not be used and might be removed in the +// far future. If you wish to stick to the legacy name validation use +// `IsValidLegacyMetricName()` and `LabelName.IsValidLegacy()` methods +// instead. This variable is here as an escape hatch for emergency cases, +// given the recent change from `LegacyValidation` to `UTF8Validation`, e.g., +// to delay UTF-8 migrations in time or aid in debugging unforeseen results of +// the change. In such a case, a temporary assignment to `LegacyValidation` +// value in the `init()` function in your main.go or so, could be considered. +// +// Historically we opted for a global variable for feature gating different +// validation schemes in operations that were not otherwise easily adjustable +// (e.g. Labels yaml unmarshaling). That could have been a mistake, a separate +// Labels structure or package might have been a better choice. Given the +// change was made and many upgraded the common already, we live this as-is +// with this warning and learning for the future. +var NameValidationScheme = UTF8Validation + +// IsValidMetricName returns true iff name matches the pattern of MetricNameRE +// for legacy names, and iff it's valid UTF-8 if scheme is UTF8Validation. +func IsValidMetricName(n LabelValue) bool { + return isValidMetricName(n, NameValidationScheme) +} diff --git a/model/metric_globalvalidationscheme_test.go b/model/metric_globalvalidationscheme_test.go new file mode 100644 index 00000000..72def1ff --- /dev/null +++ b/model/metric_globalvalidationscheme_test.go @@ -0,0 +1,38 @@ +// 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 model + +import "testing" + +func testIsValidMetricName(t *testing.T, metricName LabelValue, legacyValid, utf8Valid bool) { + t.Helper() + origScheme := NameValidationScheme + t.Cleanup(func() { + NameValidationScheme = origScheme + }) + + NameValidationScheme = LegacyValidation + if IsValidMetricName(metricName) != legacyValid { + t.Errorf("Expected %v for %q using legacy IsValidMetricName method", legacyValid, metricName) + } + if MetricNameRE.MatchString(string(metricName)) != legacyValid { + t.Errorf("Expected %v for %q using regexp matching", legacyValid, metricName) + } + NameValidationScheme = UTF8Validation + if IsValidMetricName(metricName) != utf8Valid { + t.Errorf("Expected %v for %q using UTF-8 IsValidMetricName method", utf8Valid, metricName) + } +} diff --git a/model/metric_localvalidationscheme.go b/model/metric_localvalidationscheme.go new file mode 100644 index 00000000..1e7d0d74 --- /dev/null +++ b/model/metric_localvalidationscheme.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 model + +// IsValidMetricName returns true iff name matches the pattern of MetricNameRE +// for legacy names, and iff it's valid UTF-8 if scheme is UTF8Validation. +func IsValidMetricName(n LabelValue, scheme ValidationScheme) bool { + return isValidMetricName(n, scheme) +} diff --git a/model/metric_localvalidationscheme_test.go b/model/metric_localvalidationscheme_test.go new file mode 100644 index 00000000..9f4e97e0 --- /dev/null +++ b/model/metric_localvalidationscheme_test.go @@ -0,0 +1,32 @@ +// 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 model + +import "testing" + +func testIsValidMetricName(t *testing.T, metricName LabelValue, legacyValid, utf8Valid bool) { + t.Helper() + + if IsValidMetricName(metricName, LegacyValidation) != legacyValid { + t.Errorf("Expected %v for %q using legacy IsValidMetricName method", legacyValid, metricName) + } + if MetricNameRE.MatchString(string(metricName)) != legacyValid { + t.Errorf("Expected %v for %q using regexp matching", legacyValid, metricName) + } + if IsValidMetricName(metricName, UTF8Validation) != utf8Valid { + t.Errorf("Expected %v for %q using UTF-8 IsValidMetricName method", utf8Valid, metricName) + } +} diff --git a/model/metric_test.go b/model/metric_test.go index 662a53d5..231c099b 100644 --- a/model/metric_test.go +++ b/model/metric_test.go @@ -261,17 +261,7 @@ func TestMetricNameIsLegacyValid(t *testing.T) { } for _, s := range scenarios { - NameValidationScheme = LegacyValidation - if IsValidMetricName(s.mn) != s.legacyValid { - t.Errorf("Expected %v for %q using legacy IsValidMetricName method", s.legacyValid, s.mn) - } - if MetricNameRE.MatchString(string(s.mn)) != s.legacyValid { - t.Errorf("Expected %v for %q using regexp matching", s.legacyValid, s.mn) - } - NameValidationScheme = UTF8Validation - if IsValidMetricName(s.mn) != s.utf8Valid { - t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.legacyValid, s.mn) - } + testIsValidMetricName(t, s.mn, s.legacyValid, s.utf8Valid) } } diff --git a/model/silence.go b/model/silence.go index 8f91a970..b5b2e21a 100644 --- a/model/silence.go +++ b/model/silence.go @@ -45,9 +45,8 @@ func (m *Matcher) UnmarshalJSON(b []byte) error { return nil } -// Validate returns true iff all fields of the matcher have valid values. -func (m *Matcher) Validate() error { - if !m.Name.IsValid() { +func (m *Matcher) validate(scheme ValidationScheme) error { + if !m.Name.isValid(scheme) { return fmt.Errorf("invalid name %q", m.Name) } if m.IsRegex { @@ -76,12 +75,12 @@ type Silence struct { } // Validate returns true iff all fields of the silence have valid values. -func (s *Silence) Validate() error { +func (s *Silence) validate(scheme ValidationScheme) error { if len(s.Matchers) == 0 { return errors.New("at least one matcher required") } for _, m := range s.Matchers { - if err := m.Validate(); err != nil { + if err := m.validate(scheme); err != nil { return fmt.Errorf("invalid matcher: %w", err) } } diff --git a/model/silence_globalvalidationscheme.go b/model/silence_globalvalidationscheme.go new file mode 100644 index 00000000..fd6c2308 --- /dev/null +++ b/model/silence_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 model + +// Validate returns true iff all fields of the matcher have valid values. +func (m *Matcher) Validate() error { + return m.validate(NameValidationScheme) +} + +// Validate returns true iff all fields of the silence have valid values. +func (s *Silence) Validate() error { + return s.validate(NameValidationScheme) +} diff --git a/model/silence_globalvalidationscheme_test.go b/model/silence_globalvalidationscheme_test.go new file mode 100644 index 00000000..fd0da3f0 --- /dev/null +++ b/model/silence_globalvalidationscheme_test.go @@ -0,0 +1,34 @@ +// 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 model + +func validateMatcher(matcher *Matcher, scheme ValidationScheme) error { + origScheme := NameValidationScheme + defer func() { + NameValidationScheme = origScheme + }() + NameValidationScheme = scheme + return matcher.Validate() +} + +func validateSilence(silence *Silence, scheme ValidationScheme) error { + origScheme := NameValidationScheme + defer func() { + NameValidationScheme = origScheme + }() + NameValidationScheme = scheme + return silence.Validate() +} diff --git a/model/silence_localvalidationscheme.go b/model/silence_localvalidationscheme.go new file mode 100644 index 00000000..ed2889e4 --- /dev/null +++ b/model/silence_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 model + +// Validate returns true iff all fields of the matcher have valid values. +func (m *Matcher) Validate(scheme ValidationScheme) error { + return m.validate(scheme) +} + +// Validate returns true iff all fields of the silence have valid values. +func (s *Silence) Validate(scheme ValidationScheme) error { + return s.validate(scheme) +} diff --git a/model/silence_localvalidationscheme_test.go b/model/silence_localvalidationscheme_test.go new file mode 100644 index 00000000..50024aed --- /dev/null +++ b/model/silence_localvalidationscheme_test.go @@ -0,0 +1,24 @@ +// 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 model + +func validateMatcher(matcher *Matcher, scheme ValidationScheme) error { + return matcher.Validate(scheme) +} + +func validateSilence(silence *Silence, scheme ValidationScheme) error { + return silence.Validate(scheme) +} diff --git a/model/silence_test.go b/model/silence_test.go index 4e4508de..76eb580c 100644 --- a/model/silence_test.go +++ b/model/silence_test.go @@ -80,10 +80,8 @@ func TestMatcherValidate(t *testing.T) { } for i, c := range cases { - NameValidationScheme = LegacyValidation - legacyErr := c.matcher.Validate() - NameValidationScheme = UTF8Validation - utf8Err := c.matcher.Validate() + legacyErr := validateMatcher(c.matcher, LegacyValidation) + utf8Err := validateMatcher(c.matcher, UTF8Validation) if legacyErr == nil && utf8Err == nil { if c.legacyErr == "" && c.utf8Err == "" { continue @@ -248,8 +246,7 @@ func TestSilenceValidate(t *testing.T) { } for i, c := range cases { - NameValidationScheme = LegacyValidation - err := c.sil.Validate() + err := validateSilence(c.sil, LegacyValidation) if err == nil { if c.err == "" { continue