diff --git a/expfmt/decode.go b/expfmt/decode.go index 1448439b7..425114f3c 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 759ff7461..b5e34b5e2 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 366bf8312..09de324b0 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 7a637f691..bdda4b961 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 460f554f2..e6a677b44 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 fc3eaf108..06e76a4d0 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 e2ff83595..18c12c6a0 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.String())) } } @@ -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 233954326..521c83088 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 d0ad88da3..68408abf4 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 7334b0a06..fe4c4e53b 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 2bd913fff..bdae1e3b2 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,11 @@ 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.String())) } } -// IsValidLegacyMetricName is similar to IsValidMetricName but always uses the -// legacy validation scheme regardless of the value of NameValidationScheme. +// IsValidLegacyMetricName is similar to IsValidMetricName but always uses LegacyValidation. // 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 662a53d56..24f76d5ca 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 8f91a9702..dd11298b1 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 4e4508dee..f93fd2751 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