From 96e3d9ca8098bf12a8eda77edf5dd3b3e62f2a40 Mon Sep 17 00:00:00 2001 From: Julius Hinze Date: Fri, 20 Jun 2025 17:43:35 +0200 Subject: [PATCH 1/8] feat(model): remove deprecated global NameValidationScheme Signed-off-by: Julius Hinze --- expfmt/decode.go | 14 ++++++------ expfmt/decode_test.go | 12 +++++------ go.mod | 2 ++ go.sum | 6 ++---- model/alert.go | 6 +++--- model/alert_test.go | 36 ++++++++++++++++++------------- model/labels.go | 15 +++++++------ model/labels_test.go | 6 ++---- model/labelset.go | 7 +++--- model/labelset_test.go | 10 ++++----- model/metric.go | 48 +++++++++++------------------------------- model/metric_test.go | 6 ++---- model/silence.go | 8 +++---- model/silence_test.go | 9 +++----- 14 files changed, 80 insertions(+), 105 deletions(-) diff --git a/expfmt/decode.go b/expfmt/decode.go index 1448439b..425114f3 100644 --- a/expfmt/decode.go +++ b/expfmt/decode.go @@ -29,7 +29,7 @@ import ( // Decoder types decode an input stream into metric families. type Decoder interface { - Decode(*dto.MetricFamily) error + Decode(*dto.MetricFamily, model.ValidationScheme) error } // DecodeOptions contains options used by the Decoder and in sample extraction. @@ -86,14 +86,14 @@ type protoDecoder struct { } // Decode implements the Decoder interface. -func (d *protoDecoder) Decode(v *dto.MetricFamily) error { +func (d *protoDecoder) Decode(v *dto.MetricFamily, nameValidationScheme model.ValidationScheme) error { opts := protodelim.UnmarshalOptions{ MaxSize: -1, } if err := opts.UnmarshalFrom(d.r, v); err != nil { return err } - if !model.IsValidMetricName(model.LabelValue(v.GetName())) { + if !model.IsValidMetricName(model.LabelValue(v.GetName()), nameValidationScheme) { return fmt.Errorf("invalid metric name %q", v.GetName()) } for _, m := range v.GetMetric() { @@ -107,7 +107,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 !model.LabelName(l.GetName()).IsValid(nameValidationScheme) { return fmt.Errorf("invalid label name %q", l.GetName()) } } @@ -123,7 +123,7 @@ type textDecoder struct { } // Decode implements the Decoder interface. -func (d *textDecoder) Decode(v *dto.MetricFamily) error { +func (d *textDecoder) Decode(v *dto.MetricFamily, _ model.ValidationScheme) error { if d.err == nil { // Read all metrics in one shot. var p TextParser @@ -156,8 +156,8 @@ type SampleDecoder struct { // Decode calls the Decode method of the wrapped Decoder and then extracts the // samples from the decoded MetricFamily into the provided model.Vector. -func (sd *SampleDecoder) Decode(s *model.Vector) error { - err := sd.Dec.Decode(&sd.f) +func (sd *SampleDecoder) Decode(s *model.Vector, nameValidationScheme model.ValidationScheme) error { + err := sd.Dec.Decode(&sd.f, nameValidationScheme) if err != nil { return err } diff --git a/expfmt/decode_test.go b/expfmt/decode_test.go index 759ff746..b5e34b5e 100644 --- a/expfmt/decode_test.go +++ b/expfmt/decode_test.go @@ -88,7 +88,7 @@ mf2 4 var all model.Vector for { var smpls model.Vector - err := dec.Decode(&smpls) + err := dec.Decode(&smpls, model.UTF8Validation) if err != nil && errors.Is(err, io.EOF) { break } @@ -369,22 +369,20 @@ func TestProtoDecoder(t *testing.T) { var all model.Vector for { - model.NameValidationScheme = model.LegacyValidation //nolint:staticcheck var smpls model.Vector - err := dec.Decode(&smpls) + err := dec.Decode(&smpls, model.LegacyValidation) 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") - model.NameValidationScheme = model.UTF8Validation //nolint:staticcheck dec = &SampleDecoder{ Dec: &protoDecoder{r: strings.NewReader(scenario.in)}, Opts: &DecodeOptions{ Timestamp: testTime, }, } - err = dec.Decode(&smpls) + err = dec.Decode(&smpls, model.UTF8Validation) if errors.Is(err, io.EOF) { break } @@ -412,7 +410,7 @@ func TestProtoMultiMessageDecoder(t *testing.T) { var metrics []*dto.MetricFamily for { var mf dto.MetricFamily - if err := decoder.Decode(&mf); err != nil { + if err := decoder.Decode(&mf, model.UTF8Validation); err != nil { if errors.Is(err, io.EOF) { break } @@ -560,7 +558,7 @@ func TestTextDecoderWithBufioReader(t *testing.T) { dec := NewDecoder(r, FmtText) for { var mf dto.MetricFamily - if err := dec.Decode(&mf); err != nil { + if err := dec.Decode(&mf, model.UTF8Validation); err != nil { if errors.Is(err, io.EOF) { break } diff --git a/go.mod b/go.mod index 1381a9bb..7baa3bd0 100644 --- a/go.mod +++ b/go.mod @@ -36,3 +36,5 @@ require ( ) retract v0.50.0 // Critical bug in counter suffixes, please read issue https://github.com/prometheus/common/issues/605 + +replace github.com/prometheus/client_golang => github.com/juliusmh/client_golang v1.22.1-0.20250701110037-ceb5803cbf1f diff --git a/go.sum b/go.sum index 3c89e304..0cbea484 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,8 @@ 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/juliusmh/client_golang v1.22.1-0.20250701110037-ceb5803cbf1f h1:UU3kYZyItj1WIp7nfjBrw+S9xjyEX1NbHgp1UP3Ov6U= +github.com/juliusmh/client_golang v1.22.1-0.20250701110037-ceb5803cbf1f/go.mod h1:tF4MYJHY3axE4Wh1TgNU/klT0a4RUGthK8Chg9eU/sA= 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,8 +29,6 @@ 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_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= diff --git a/model/alert.go b/model/alert.go index 460f554f..e6a677b4 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(nameValidationScheme 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(nameValidationScheme); 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(nameValidationScheme); err != nil { return fmt.Errorf("invalid annotations: %w", err) } return nil diff --git a/model/alert_test.go b/model/alert_test.go index fc3eaf10..06e76a4d 100644 --- a/model/alert_test.go +++ b/model/alert_test.go @@ -22,28 +22,26 @@ import ( ) func TestAlertValidate(t *testing.T) { - oldScheme := NameValidationScheme - NameValidationScheme = LegacyValidation - defer func() { - NameValidationScheme = oldScheme - }() ts := time.Now() cases := []struct { - alert *Alert - err string + alert *Alert + err string + scheme ValidationScheme }{ { alert: &Alert{ Labels: LabelSet{"a": "b"}, StartsAt: ts, }, + scheme: LegacyValidation, }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, }, - err: "start time missing", + scheme: LegacyValidation, + err: "start time missing", }, { alert: &Alert{ @@ -51,6 +49,7 @@ func TestAlertValidate(t *testing.T) { StartsAt: ts, EndsAt: ts, }, + scheme: LegacyValidation, }, { alert: &Alert{ @@ -58,6 +57,7 @@ func TestAlertValidate(t *testing.T) { StartsAt: ts, EndsAt: ts.Add(1 * time.Minute), }, + scheme: LegacyValidation, }, { alert: &Alert{ @@ -65,27 +65,31 @@ func TestAlertValidate(t *testing.T) { StartsAt: ts, EndsAt: ts.Add(-1 * time.Minute), }, - err: "start time must be before end time", + scheme: LegacyValidation, + err: "start time must be before end time", }, { alert: &Alert{ StartsAt: ts, }, - err: "at least one label pair required", + scheme: LegacyValidation, + err: "at least one label pair required", }, { alert: &Alert{ Labels: LabelSet{"a": "b", "!bad": "label"}, StartsAt: ts, }, - err: "invalid label set: invalid name", + scheme: LegacyValidation, + err: "invalid label set: invalid name", }, { alert: &Alert{ Labels: LabelSet{"a": "b", "bad": "\xfflabel"}, StartsAt: ts, }, - err: "invalid label set: invalid value", + scheme: LegacyValidation, + err: "invalid label set: invalid value", }, { alert: &Alert{ @@ -93,7 +97,8 @@ func TestAlertValidate(t *testing.T) { Annotations: LabelSet{"!bad": "label"}, StartsAt: ts, }, - err: "invalid annotations: invalid name", + scheme: LegacyValidation, + err: "invalid annotations: invalid name", }, { alert: &Alert{ @@ -101,12 +106,13 @@ func TestAlertValidate(t *testing.T) { Annotations: LabelSet{"bad": "\xfflabel"}, StartsAt: ts, }, - err: "invalid annotations: invalid value", + scheme: LegacyValidation, + err: "invalid annotations: invalid value", }, } for i, c := range cases { - err := c.alert.Validate() + err := c.alert.Validate(c.scheme) if err == nil { if c.err == "" { continue diff --git a/model/labels.go b/model/labels.go index e2ff8359..a203dc4a 100644 --- a/model/labels.go +++ b/model/labels.go @@ -104,19 +104,18 @@ var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") 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 { +// scheme is LegacyValidation, or valid UTF-8 if it is UTF8Validation. +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)) } } @@ -137,12 +136,13 @@ func (ln LabelName) IsValidLegacy() bool { } // 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() { + if !LabelName(s).IsValid(UTF8Validation) { return fmt.Errorf("%q is not a valid label name", s) } *ln = LabelName(s) @@ -150,12 +150,13 @@ func (ln *LabelName) UnmarshalYAML(unmarshal func(interface{}) error) error { } // 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() { + if !LabelName(s).IsValid(UTF8Validation) { return fmt.Errorf("%q is not a valid label name", s) } *ln = LabelName(s) diff --git a/model/labels_test.go b/model/labels_test.go index 23395432..521c8308 100644 --- a/model/labels_test.go +++ b/model/labels_test.go @@ -144,15 +144,13 @@ func TestLabelNameIsValid(t *testing.T) { } for _, s := range scenarios { - NameValidationScheme = LegacyValidation - if s.ln.IsValid() != s.legacyValid { + if s.ln.IsValid(LegacyValidation) != 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 { + if s.ln.IsValid(UTF8Validation) != s.utf8Valid { t.Errorf("Expected %v for %q using UTF-8 IsValid method", s.legacyValid, s.ln) } } diff --git a/model/labelset.go b/model/labelset.go index d0ad88da..68408abf 100644 --- a/model/labelset.go +++ b/model/labelset.go @@ -28,9 +28,9 @@ 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(nameValidationScheme ValidationScheme) error { for ln, lv := range ls { - if !ln.IsValid() { + if !ln.IsValid(nameValidationScheme) { return fmt.Errorf("invalid name %q", ln) } if !lv.IsValid() { @@ -140,6 +140,7 @@ func (ls LabelSet) FastFingerprint() Fingerprint { } // 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 { @@ -149,7 +150,7 @@ func (l *LabelSet) UnmarshalJSON(b []byte) error { // 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() { + if !ln.IsValid(UTF8Validation) { return fmt.Errorf("%q is not a valid label name", ln) } } diff --git a/model/labelset_test.go b/model/labelset_test.go index 7334b0a0..fe4c4e53 100644 --- a/model/labelset_test.go +++ b/model/labelset_test.go @@ -16,6 +16,8 @@ package model import ( "encoding/json" "testing" + + "github.com/stretchr/testify/require" ) func TestUnmarshalJSONLabelSet(t *testing.T) { @@ -55,12 +57,10 @@ func TestUnmarshalJSONLabelSet(t *testing.T) { } }` - NameValidationScheme = LegacyValidation err = json.Unmarshal([]byte(invalidlabelSetJSON), &c) - expectedErr := `"1nvalid_23name" is not a valid label name` - if err == nil || err.Error() != expectedErr { - t.Errorf("expected an error with message '%s' to be thrown", expectedErr) - } + require.NoError(t, err) + err = c.LabelSet.Validate(LegacyValidation) + require.EqualError(t, err, `invalid name "1nvalid_23name"`) } func TestLabelSetClone(t *testing.T) { diff --git a/model/metric.go b/model/metric.go index 2bd913ff..800bf160 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. @@ -228,10 +205,9 @@ func (m Metric) FastFingerprint() Fingerprint { } // 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 { +// for legacy names, and iff it's valid UTF-8 if scheme is UTF8Validation. +func IsValidMetricName(n LabelValue, scheme ValidationScheme) bool { + switch scheme { case LegacyValidation: return IsValidLegacyMetricName(string(n)) case UTF8Validation: @@ -240,12 +216,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_test.go b/model/metric_test.go index 662a53d5..24f76d5c 100644 --- a/model/metric_test.go +++ b/model/metric_test.go @@ -261,15 +261,13 @@ func TestMetricNameIsLegacyValid(t *testing.T) { } for _, s := range scenarios { - NameValidationScheme = LegacyValidation - if IsValidMetricName(s.mn) != s.legacyValid { + if IsValidMetricName(s.mn, LegacyValidation) != 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 { + if IsValidMetricName(s.mn, UTF8Validation) != s.utf8Valid { t.Errorf("Expected %v for %q using utf-8 IsValidMetricName method", s.legacyValid, s.mn) } } diff --git a/model/silence.go b/model/silence.go index 8f91a970..dd11298b 100644 --- a/model/silence.go +++ b/model/silence.go @@ -46,8 +46,8 @@ func (m *Matcher) UnmarshalJSON(b []byte) error { } // 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(nameValidationScheme ValidationScheme) error { + if !m.Name.IsValid(nameValidationScheme) { return fmt.Errorf("invalid name %q", m.Name) } if m.IsRegex { @@ -76,12 +76,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(nameValidationScheme 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(nameValidationScheme); err != nil { return fmt.Errorf("invalid matcher: %w", err) } } diff --git a/model/silence_test.go b/model/silence_test.go index 4e4508de..f93fd275 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 := c.matcher.Validate(LegacyValidation) + utf8Err := c.matcher.Validate(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 := c.sil.Validate(LegacyValidation) if err == nil { if c.err == "" { continue From c6ae72fb63e94d640c885b4745508127f86bfb22 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Sun, 13 Jul 2025 11:36:45 +0200 Subject: [PATCH 2/8] Move changes behind build tag Signed-off-by: Arve Knudsen --- expfmt/decode.go | 30 ++------ expfmt/decode_globalvalidationscheme.go | 48 ++++++++++++ expfmt/decode_globalvalidationscheme_test.go | 36 +++++++++ expfmt/decode_localvalidationscheme.go | 52 +++++++++++++ expfmt/decode_localvalidationscheme_test.go | 27 +++++++ expfmt/decode_test.go | 79 ++++++++++---------- go.mod | 6 +- go.sum | 10 ++- model/alert.go | 6 +- model/alert_globalvalidationscheme.go | 21 ++++++ model/alert_globalvalidationscheme_test.go | 29 +++++++ model/alert_localvalidationscheme.go | 21 ++++++ model/alert_localvalidationscheme_test.go | 22 ++++++ model/alert_test.go | 58 +++++++------- model/labels.go | 38 ++-------- model/labels_globalvalidationscheme.go | 53 +++++++++++++ model/labels_globalvalidationscheme_test.go | 39 ++++++++++ model/labels_localvalidationscheme.go | 55 ++++++++++++++ model/labels_localvalidationscheme_test.go | 32 ++++++++ model/labels_test.go | 10 +-- model/labelset.go | 28 ++----- model/labelset_globalvalidationscheme.go | 45 +++++++++++ model/labelset_localvalidationscheme.go | 46 ++++++++++++ model/labelset_test.go | 2 +- model/metric.go | 4 +- model/metric_globalvalidationscheme.go | 43 +++++++++++ model/metric_globalvalidationscheme_test.go | 38 ++++++++++ model/metric_localvalidationscheme.go | 22 ++++++ model/metric_localvalidationscheme_test.go | 32 ++++++++ model/metric_test.go | 10 +-- model/silence.go | 9 +-- model/silence_globalvalidationscheme.go | 26 +++++++ model/silence_globalvalidationscheme_test.go | 34 +++++++++ model/silence_localvalidationscheme.go | 26 +++++++ model/silence_localvalidationscheme_test.go | 24 ++++++ model/silence_test.go | 6 +- 36 files changed, 880 insertions(+), 187 deletions(-) create mode 100644 expfmt/decode_globalvalidationscheme.go create mode 100644 expfmt/decode_globalvalidationscheme_test.go create mode 100644 expfmt/decode_localvalidationscheme.go create mode 100644 expfmt/decode_localvalidationscheme_test.go create mode 100644 model/alert_globalvalidationscheme.go create mode 100644 model/alert_globalvalidationscheme_test.go create mode 100644 model/alert_localvalidationscheme.go create mode 100644 model/alert_localvalidationscheme_test.go create mode 100644 model/labels_globalvalidationscheme.go create mode 100644 model/labels_globalvalidationscheme_test.go create mode 100644 model/labels_localvalidationscheme.go create mode 100644 model/labels_localvalidationscheme_test.go create mode 100644 model/labelset_globalvalidationscheme.go create mode 100644 model/labelset_localvalidationscheme.go create mode 100644 model/metric_globalvalidationscheme.go create mode 100644 model/metric_globalvalidationscheme_test.go create mode 100644 model/metric_localvalidationscheme.go create mode 100644 model/metric_localvalidationscheme_test.go create mode 100644 model/silence_globalvalidationscheme.go create mode 100644 model/silence_globalvalidationscheme_test.go create mode 100644 model/silence_localvalidationscheme.go create mode 100644 model/silence_localvalidationscheme_test.go diff --git a/expfmt/decode.go b/expfmt/decode.go index 425114f3..8e1b02f0 100644 --- a/expfmt/decode.go +++ b/expfmt/decode.go @@ -14,7 +14,6 @@ package expfmt import ( - "bufio" "fmt" "io" "math" @@ -29,7 +28,7 @@ import ( // Decoder types decode an input stream into metric families. type Decoder interface { - Decode(*dto.MetricFamily, model.ValidationScheme) error + Decode(*dto.MetricFamily) error } // DecodeOptions contains options used by the Decoder and in sample extraction. @@ -70,30 +69,15 @@ 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, nameValidationScheme model.ValidationScheme) error { +func (d *protoDecoder) Decode(v *dto.MetricFamily) error { opts := protodelim.UnmarshalOptions{ MaxSize: -1, } if err := opts.UnmarshalFrom(d.r, v); err != nil { return err } - if !model.IsValidMetricName(model.LabelValue(v.GetName()), nameValidationScheme) { + 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, nameValidationScheme model.Va if !model.LabelValue(l.GetValue()).IsValid() { return fmt.Errorf("invalid label value %q", l.GetValue()) } - if !model.LabelName(l.GetName()).IsValid(nameValidationScheme) { + if !d.isValidLabelName(l.GetName()) { return fmt.Errorf("invalid label name %q", l.GetName()) } } @@ -123,7 +107,7 @@ type textDecoder struct { } // Decode implements the Decoder interface. -func (d *textDecoder) Decode(v *dto.MetricFamily, _ model.ValidationScheme) error { +func (d *textDecoder) Decode(v *dto.MetricFamily) error { if d.err == nil { // Read all metrics in one shot. var p TextParser @@ -156,8 +140,8 @@ type SampleDecoder struct { // Decode calls the Decode method of the wrapped Decoder and then extracts the // samples from the decoded MetricFamily into the provided model.Vector. -func (sd *SampleDecoder) Decode(s *model.Vector, nameValidationScheme model.ValidationScheme) error { - err := sd.Dec.Decode(&sd.f, nameValidationScheme) +func (sd *SampleDecoder) Decode(s *model.Vector) error { + err := sd.Dec.Decode(&sd.f) if err != nil { return err } 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 b5e34b5e..1374bc83 100644 --- a/expfmt/decode_test.go +++ b/expfmt/decode_test.go @@ -23,6 +23,7 @@ import ( "os" "reflect" "sort" + "strconv" "strings" "testing" @@ -88,7 +89,7 @@ mf2 4 var all model.Vector for { var smpls model.Vector - err := dec.Decode(&smpls, model.UTF8Validation) + err := dec.Decode(&smpls) if err != nil && errors.Is(err, io.EOF) { break } @@ -360,44 +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 { - var smpls model.Vector - err := dec.Decode(&smpls, model.LegacyValidation) - 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") - 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, model.UTF8Validation) - 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) + }) } } @@ -406,11 +409,11 @@ 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 - if err := decoder.Decode(&mf, model.UTF8Validation); err != nil { + if err := decoder.Decode(&mf); err != nil { if errors.Is(err, io.EOF) { break } @@ -555,10 +558,10 @@ 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, model.UTF8Validation); err != nil { + if err := dec.Decode(&mf); err != nil { if errors.Is(err, io.EOF) { break } diff --git a/go.mod b/go.mod index 7baa3bd0..c7f7bad8 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.20250714085536-7c8795190db3 // 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 @@ -36,5 +36,3 @@ require ( ) retract v0.50.0 // Critical bug in counter suffixes, please read issue https://github.com/prometheus/common/issues/605 - -replace github.com/prometheus/client_golang => github.com/juliusmh/client_golang v1.22.1-0.20250701110037-ceb5803cbf1f diff --git a/go.sum b/go.sum index 0cbea484..e5a5b7c6 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/juliusmh/client_golang v1.22.1-0.20250701110037-ceb5803cbf1f h1:UU3kYZyItj1WIp7nfjBrw+S9xjyEX1NbHgp1UP3Ov6U= -github.com/juliusmh/client_golang v1.22.1-0.20250701110037-ceb5803cbf1f/go.mod h1:tF4MYJHY3axE4Wh1TgNU/klT0a4RUGthK8Chg9eU/sA= 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,10 +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.22.1-0.20250714085536-7c8795190db3 h1:TZI7quz/49D+lf+bVNeMCoOBtmXJ1ogYKLKM1DHdb8U= +github.com/prometheus/client_golang v1.22.1-0.20250714085536-7c8795190db3/go.mod h1:AQUOFBdlbVQEsbJd2DSCFcDs1I51B8lEI3/M0JMDEZA= 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= @@ -41,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 e6a677b4..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(nameValidationScheme ValidationScheme) 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(nameValidationScheme); 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(nameValidationScheme); 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 06e76a4d..cc0f2fa5 100644 --- a/model/alert_test.go +++ b/model/alert_test.go @@ -16,6 +16,7 @@ package model import ( "fmt" "sort" + "strconv" "strings" "testing" "time" @@ -25,23 +26,20 @@ func TestAlertValidate(t *testing.T) { ts := time.Now() cases := []struct { - alert *Alert - err string - scheme ValidationScheme + alert *Alert + err string }{ { alert: &Alert{ Labels: LabelSet{"a": "b"}, StartsAt: ts, }, - scheme: LegacyValidation, }, { alert: &Alert{ Labels: LabelSet{"a": "b"}, }, - scheme: LegacyValidation, - err: "start time missing", + err: "start time missing", }, { alert: &Alert{ @@ -49,7 +47,6 @@ func TestAlertValidate(t *testing.T) { StartsAt: ts, EndsAt: ts, }, - scheme: LegacyValidation, }, { alert: &Alert{ @@ -57,7 +54,6 @@ func TestAlertValidate(t *testing.T) { StartsAt: ts, EndsAt: ts.Add(1 * time.Minute), }, - scheme: LegacyValidation, }, { alert: &Alert{ @@ -65,31 +61,27 @@ func TestAlertValidate(t *testing.T) { StartsAt: ts, EndsAt: ts.Add(-1 * time.Minute), }, - scheme: LegacyValidation, - err: "start time must be before end time", + err: "start time must be before end time", }, { alert: &Alert{ StartsAt: ts, }, - scheme: LegacyValidation, - err: "at least one label pair required", + err: "at least one label pair required", }, { alert: &Alert{ Labels: LabelSet{"a": "b", "!bad": "label"}, StartsAt: ts, }, - scheme: LegacyValidation, - err: "invalid label set: invalid name", + err: "invalid label set: invalid name", }, { alert: &Alert{ Labels: LabelSet{"a": "b", "bad": "\xfflabel"}, StartsAt: ts, }, - scheme: LegacyValidation, - err: "invalid label set: invalid value", + err: "invalid label set: invalid value", }, { alert: &Alert{ @@ -97,8 +89,7 @@ func TestAlertValidate(t *testing.T) { Annotations: LabelSet{"!bad": "label"}, StartsAt: ts, }, - scheme: LegacyValidation, - err: "invalid annotations: invalid name", + err: "invalid annotations: invalid name", }, { alert: &Alert{ @@ -106,27 +97,28 @@ func TestAlertValidate(t *testing.T) { Annotations: LabelSet{"bad": "\xfflabel"}, StartsAt: ts, }, - scheme: LegacyValidation, - err: "invalid annotations: invalid value", + err: "invalid annotations: invalid value", }, } for i, c := range cases { - err := c.alert.Validate(c.scheme) - 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 a203dc4a..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,9 +105,7 @@ 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 -// scheme is LegacyValidation, or valid UTF-8 if it is UTF8Validation. -func (ln LabelName) IsValid(scheme ValidationScheme) bool { +func (ln LabelName) isValid(scheme ValidationScheme) bool { if len(ln) == 0 { return false } @@ -135,33 +135,11 @@ func (ln LabelName) IsValidLegacy() bool { return true } -// 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 -} +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 521c8308..ef8c459a 100644 --- a/model/labels_test.go +++ b/model/labels_test.go @@ -144,15 +144,7 @@ func TestLabelNameIsValid(t *testing.T) { } for _, s := range scenarios { - if s.ln.IsValid(LegacyValidation) != 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) - } - if s.ln.IsValid(UTF8Validation) != 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 68408abf..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(nameValidationScheme ValidationScheme) error { +func (ls LabelSet) validate(scheme ValidationScheme) error { for ln, lv := range ls { - if !ln.IsValid(nameValidationScheme) { + if !ln.isValid(scheme) { return fmt.Errorf("invalid name %q", ln) } if !lv.IsValid() { @@ -139,21 +137,7 @@ func (ls LabelSet) FastFingerprint() Fingerprint { return labelSetToFastFingerprint(ls) } -// 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 -} +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 fe4c4e53..01f12b0a 100644 --- a/model/labelset_test.go +++ b/model/labelset_test.go @@ -59,7 +59,7 @@ func TestUnmarshalJSONLabelSet(t *testing.T) { err = json.Unmarshal([]byte(invalidlabelSetJSON), &c) require.NoError(t, err) - err = c.LabelSet.Validate(LegacyValidation) + err = c.LabelSet.validate(LegacyValidation) require.EqualError(t, err, `invalid name "1nvalid_23name"`) } diff --git a/model/metric.go b/model/metric.go index 800bf160..abeb31cb 100644 --- a/model/metric.go +++ b/model/metric.go @@ -204,9 +204,7 @@ 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 scheme is UTF8Validation. -func IsValidMetricName(n LabelValue, scheme ValidationScheme) bool { +func isValidMetricName(n LabelValue, scheme ValidationScheme) bool { switch scheme { case LegacyValidation: return IsValidLegacyMetricName(string(n)) 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 24f76d5c..231c099b 100644 --- a/model/metric_test.go +++ b/model/metric_test.go @@ -261,15 +261,7 @@ func TestMetricNameIsLegacyValid(t *testing.T) { } for _, s := range scenarios { - if IsValidMetricName(s.mn, LegacyValidation) != 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) - } - if IsValidMetricName(s.mn, UTF8Validation) != 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 dd11298b..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(nameValidationScheme ValidationScheme) error { - if !m.Name.IsValid(nameValidationScheme) { +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(nameValidationScheme ValidationScheme) 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(nameValidationScheme); 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 f93fd275..76eb580c 100644 --- a/model/silence_test.go +++ b/model/silence_test.go @@ -80,8 +80,8 @@ func TestMatcherValidate(t *testing.T) { } for i, c := range cases { - legacyErr := c.matcher.Validate(LegacyValidation) - utf8Err := c.matcher.Validate(UTF8Validation) + legacyErr := validateMatcher(c.matcher, LegacyValidation) + utf8Err := validateMatcher(c.matcher, UTF8Validation) if legacyErr == nil && utf8Err == nil { if c.legacyErr == "" && c.utf8Err == "" { continue @@ -246,7 +246,7 @@ func TestSilenceValidate(t *testing.T) { } for i, c := range cases { - err := c.sil.Validate(LegacyValidation) + err := validateSilence(c.sil, LegacyValidation) if err == nil { if c.err == "" { continue From cf1958fb7c9d47ca0151a476ed38a362314a274c Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 14 Jul 2025 11:00:59 +0200 Subject: [PATCH 3/8] Upgrade client_golang Signed-off-by: Arve Knudsen --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c7f7bad8..a5612a90 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ 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.22.1-0.20250714085536-7c8795190db3 // indirect + github.com/prometheus/client_golang v1.22.1-0.20250714085946-056a7b9a8ca2 // 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 diff --git a/go.sum b/go.sum index e5a5b7c6..3be28d0a 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ 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.22.1-0.20250714085536-7c8795190db3 h1:TZI7quz/49D+lf+bVNeMCoOBtmXJ1ogYKLKM1DHdb8U= -github.com/prometheus/client_golang v1.22.1-0.20250714085536-7c8795190db3/go.mod h1:AQUOFBdlbVQEsbJd2DSCFcDs1I51B8lEI3/M0JMDEZA= +github.com/prometheus/client_golang v1.22.1-0.20250714085946-056a7b9a8ca2 h1:U3tn41aN8oT6gE44szWlFYjWDPj1vTg9xFq8JAsRMTc= +github.com/prometheus/client_golang v1.22.1-0.20250714085946-056a7b9a8ca2/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.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= From 547032299364f8f6f088a8ed06f498182383d09d Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 14 Jul 2025 11:05:41 +0200 Subject: [PATCH 4/8] Update CircleCI matrix Signed-off-by: Arve Knudsen --- .circleci/config.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) 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: From 2fcab0acfe8eeec9488d2e1c606be96954c8aa9c Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 14 Jul 2025 11:45:53 +0200 Subject: [PATCH 5/8] Don't use require Signed-off-by: Arve Knudsen --- model/labelset_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/model/labelset_test.go b/model/labelset_test.go index 01f12b0a..87d3c0fb 100644 --- a/model/labelset_test.go +++ b/model/labelset_test.go @@ -16,8 +16,6 @@ package model import ( "encoding/json" "testing" - - "github.com/stretchr/testify/require" ) func TestUnmarshalJSONLabelSet(t *testing.T) { @@ -57,10 +55,14 @@ func TestUnmarshalJSONLabelSet(t *testing.T) { } }` - err = json.Unmarshal([]byte(invalidlabelSetJSON), &c) - require.NoError(t, err) + if err := json.Unmarshal([]byte(invalidlabelSetJSON), &c); err != nil { + t.Errorf("unexpected error: %s", err) + } err = c.LabelSet.validate(LegacyValidation) - require.EqualError(t, err, `invalid name "1nvalid_23name"`) + const expectedErr = `invalid name "1nvalid_23name"` + if err == nil || err.Error() != expectedErr { + t.Errorf("expected an error with message '%s' to be thrown, got: '%s'", expectedErr, err) + } } func TestLabelSetClone(t *testing.T) { From 540984d783d95041ff8b965bd53329ce9f24a378 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 14 Jul 2025 11:48:25 +0200 Subject: [PATCH 6/8] Upgrade client_golang Signed-off-by: Arve Knudsen --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a5612a90..d42cad20 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ 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.22.1-0.20250714085946-056a7b9a8ca2 // 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 diff --git a/go.sum b/go.sum index 3be28d0a..4ca937b2 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ 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.22.1-0.20250714085946-056a7b9a8ca2 h1:U3tn41aN8oT6gE44szWlFYjWDPj1vTg9xFq8JAsRMTc= -github.com/prometheus/client_golang v1.22.1-0.20250714085946-056a7b9a8ca2/go.mod h1:CR59Lx0qSGs4e5Zxr3VgiN2yuxy90eRO/0sO/iCjUH0= +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.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= From 752251bd65d47e07175064c01173d6f4fdcfe145 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 14 Jul 2025 16:57:27 +0200 Subject: [PATCH 7/8] README.md: Add section on validation scheme and build tag Signed-off-by: Arve Knudsen --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index f7d5342d..46878180 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,16 @@ 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. +For the time being, the `localvalidationscheme` build tag is experimental and the API enabled by it may change. From 3382528d0c0b25fadb8640314d92ec1728669459 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Mon, 14 Jul 2025 21:35:45 +0200 Subject: [PATCH 8/8] Update README.md Co-authored-by: Owen Williams Signed-off-by: Arve Knudsen --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 46878180..5001a34c 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,5 @@ The active name validation scheme is normally implicitly controlled via the glob 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.