diff --git a/go.mod b/go.mod index 53c154a74..f0088382c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/prometheus/client_golang -go 1.22 +go 1.23.0 + +toolchain go1.24.4 require ( github.com/beorn7/perks v1.0.1 @@ -9,11 +11,11 @@ require ( github.com/json-iterator/go v1.1.12 github.com/klauspost/compress v1.18.0 github.com/kylelemons/godebug v1.1.0 - github.com/prometheus/client_model v0.6.1 + github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.62.0 github.com/prometheus/procfs v0.15.1 - golang.org/x/sys v0.30.0 - google.golang.org/protobuf v1.36.5 + golang.org/x/sys v0.33.0 + google.golang.org/protobuf v1.36.6 ) require ( @@ -23,10 +25,12 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/text v0.25.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) exclude github.com/prometheus/client_golang v1.12.1 + +replace github.com/prometheus/common => github.com/juliusmh/common v0.65.1-0.20250701090419-a09de840631d diff --git a/go.sum b/go.sum index 449062c4b..83b4e4138 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juliusmh/common v0.65.1-0.20250701090419-a09de840631d h1:CHm2BEsxiYZetQCrVaH6NQDxbW00DHTbt9jt0Fh2+78= +github.com/juliusmh/common v0.65.1-0.20250701090419-a09de840631d/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -33,10 +35,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -46,16 +46,16 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/prometheus/desc.go b/prometheus/desc.go index ad347113c..ffb452523 100644 --- a/prometheus/desc.go +++ b/prometheus/desc.go @@ -66,6 +66,32 @@ type Desc struct { err error } +type descriptorOptions struct { + validationScheme model.ValidationScheme +} + +// newDescriptorOptions creates default descriptor options and applies opts. +func newDescriptorOptions(opts ...DescOption) *descriptorOptions { + d := &descriptorOptions{ + validationScheme: model.UTF8Validation, + } + for _, o := range opts { + o(d) + } + return d +} + +// WithValidationScheme ensures descriptor's label and metric names adhere to scheme. +// Default is UTF-8 validation. +func WithValidationScheme(scheme model.ValidationScheme) DescOption { + return func(o *descriptorOptions) { + o.validationScheme = scheme + } +} + +// DescOption are options that can be passed to NewDesc +type DescOption func(*descriptorOptions) + // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc // and will be reported on registration time. variableLabels and constLabels can // be nil if no such labels should be set. fqName must not be empty. @@ -75,8 +101,8 @@ type Desc struct { // // For constLabels, the label values are constant. Therefore, they are fully // specified in the Desc. See the Collector example for a usage pattern. -func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { - return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels) +func NewDesc(fqName, help string, variableLabels []string, constLabels Labels, opts ...DescOption) *Desc { + return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels, opts...) } // NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc @@ -89,13 +115,14 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) * // // For constLabels, the label values are constant. Therefore, they are fully // specified in the Desc. See the Collector example for a usage pattern. -func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc { +func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, opts ...DescOption) *Desc { d := &Desc{ fqName: fqName, help: help, variableLabels: variableLabels.compile(), } - if !model.IsValidMetricName(model.LabelValue(fqName)) { + descOpts := newDescriptorOptions(opts...) + if !model.IsValidMetricName(model.LabelValue(fqName), descOpts.validationScheme) { d.err = fmt.Errorf("%q is not a valid metric name", fqName) return d } @@ -107,7 +134,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const labelNameSet := map[string]struct{}{} // First add only the const label names and sort them... for labelName := range constLabels { - if !checkLabelName(labelName) { + if !checkLabelName(labelName, descOpts.validationScheme) { d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName) return d } @@ -129,7 +156,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const // cannot be in a regular label name. That prevents matching the label // dimension with a different mix between preset and variable labels. for _, label := range d.variableLabels.names { - if !checkLabelName(label) { + if !checkLabelName(label, descOpts.validationScheme) { d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName) return d } diff --git a/prometheus/desc_test.go b/prometheus/desc_test.go index 5a8429009..821668a7d 100644 --- a/prometheus/desc_test.go +++ b/prometheus/desc_test.go @@ -15,30 +15,86 @@ package prometheus import ( "testing" + + "github.com/prometheus/common/model" ) -func TestNewDescInvalidLabelValues(t *testing.T) { - desc := NewDesc( - "sample_label", - "sample label", - nil, - Labels{"a": "\xFF"}, - ) - if desc.err == nil { - t.Errorf("NewDesc: expected error because: %s", desc.err) +func TestNewDesc(t *testing.T) { + testCases := []struct { + name string + fqName string + help string + variableLabels []string + labels Labels + opts []DescOption + wantErr string + }{ + { + name: "invalid label value", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"a": "\xff"}, + wantErr: `label value "\xff" is not valid UTF-8`, + }, + { + name: "nil label values", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: nil, + }, + { + name: "invalid label name", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"\xff": "test"}, + wantErr: `"\xff" is not a valid label name for metric "sample_label"`, + }, + { + name: "invalid legacy label name", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"test😀": "test"}, + opts: []DescOption{WithValidationScheme(model.LegacyValidation)}, + wantErr: `"test😀" is not a valid label name for metric "sample_label"`, + }, + { + name: "invalid legacy metric name", + fqName: "sample_label😀", + help: "sample label", + opts: []DescOption{WithValidationScheme(model.LegacyValidation)}, + wantErr: `"sample_label😀" is not a valid metric name`, + }, + { + name: "valid utf8 label name", + fqName: "sample_label", + help: "sample label", + variableLabels: nil, + labels: Labels{"test😀": "test"}, + }, } -} - -func TestNewDescNilLabelValues(t *testing.T) { - desc := NewDesc( - "sample_label", - "sample label", - nil, - nil, - ) - if desc.err != nil { - t.Errorf("NewDesc: unexpected error: %s", desc.err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + desc := NewDesc( + tc.fqName, + tc.help, + tc.variableLabels, + tc.labels, + tc.opts..., + ) + if desc.err != nil && tc.wantErr != desc.err.Error() { + t.Fatalf("NewDesc: expected error %q but got %+v", tc.wantErr, desc.err) + } else if desc.err == nil && tc.wantErr != "" { + t.Fatalf("NewDesc: expected error %q but got nil", tc.wantErr) + } else if desc.err != nil && tc.wantErr == "" { + t.Fatalf("NewDesc: %+v", desc.err) + } + }) } + } func TestNewDescWithNilLabelValues_String(t *testing.T) { diff --git a/prometheus/labels.go b/prometheus/labels.go index c21911f29..025fbefee 100644 --- a/prometheus/labels.go +++ b/prometheus/labels.go @@ -183,6 +183,6 @@ func validateLabelValues(vals []string, expectedNumberOfValues int) error { return nil } -func checkLabelName(l string) bool { - return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix) +func checkLabelName(l string, nameValidationSheme model.ValidationScheme) bool { + return model.LabelName(l).IsValid(nameValidationSheme) && !strings.HasPrefix(l, reservedLabelPrefix) } diff --git a/prometheus/push/push.go b/prometheus/push/push.go index e524aa130..8df425ea8 100644 --- a/prometheus/push/push.go +++ b/prometheus/push/push.go @@ -70,8 +70,9 @@ type HTTPDoer interface { type Pusher struct { error error - url, job string - grouping map[string]string + url, job string + grouping map[string]string + validationScheme model.ValidationScheme gatherers prometheus.Gatherers registerer prometheus.Registerer @@ -84,11 +85,22 @@ type Pusher struct { expfmt expfmt.Format } +// Option used to create Pusher instances. +type Option func(*Pusher) + +// WithValidationScheme sets the validation used for label and metric names. +// Default is model.UTF8Validation. +func WithValidationScheme(scheme model.ValidationScheme) Option { + return func(p *Pusher) { + p.validationScheme = scheme + } +} + // New creates a new Pusher to push to the provided URL with the provided job // name (which must not be empty). You can use just host:port or ip:port as url, // in which case “http://” is added automatically. Alternatively, include the // schema in the URL. However, do not include the “/metrics/jobs/…” part. -func New(url, job string) *Pusher { +func New(url, job string, opts ...Option) *Pusher { var ( reg = prometheus.NewRegistry() err error @@ -101,16 +113,21 @@ func New(url, job string) *Pusher { } url = strings.TrimSuffix(url, "/") - return &Pusher{ - error: err, - url: url, - job: job, - grouping: map[string]string{}, - gatherers: prometheus.Gatherers{reg}, - registerer: reg, - client: &http.Client{}, - expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim), + pusher := &Pusher{ + error: err, + url: url, + job: job, + grouping: map[string]string{}, + gatherers: prometheus.Gatherers{reg}, + registerer: reg, + client: &http.Client{}, + expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim), + validationScheme: model.UTF8Validation, + } + for _, opt := range opts { + opt(pusher) } + return pusher } // Push collects/gathers all metrics from all Collectors and Gatherers added to @@ -182,7 +199,7 @@ func (p *Pusher) Error() error { // For convenience, this method returns a pointer to the Pusher itself. func (p *Pusher) Grouping(name, value string) *Pusher { if p.error == nil { - if !model.LabelName(name).IsValid() { + if !model.LabelName(name).IsValid(p.validationScheme) { p.error = fmt.Errorf("grouping label has invalid name: %s", name) return p } diff --git a/prometheus/registry.go b/prometheus/registry.go index c6fd2f58b..76948bdb8 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -31,6 +31,7 @@ import ( "github.com/cespare/xxhash/v2" dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" ) @@ -62,14 +63,27 @@ func init() { MustRegister(NewGoCollector()) } +type RegistryOption func(*Registry) + +func WithNameValidationScheme(scheme model.ValidationScheme) RegistryOption { + return func(r *Registry) { + r.nameValidationScheme = scheme + } +} + // NewRegistry creates a new vanilla Registry without any Collectors // pre-registered. -func NewRegistry() *Registry { - return &Registry{ - collectorsByID: map[uint64]Collector{}, - descIDs: map[uint64]struct{}{}, - dimHashesByName: map[string]uint64{}, +func NewRegistry(opts ...RegistryOption) *Registry { + reg := &Registry{ + collectorsByID: map[uint64]Collector{}, + descIDs: map[uint64]struct{}{}, + dimHashesByName: map[string]uint64{}, + nameValidationScheme: model.UTF8Validation, + } + for _, opt := range opts { + opt(reg) } + return reg } // NewPedanticRegistry returns a registry that checks during collection if each @@ -264,6 +278,7 @@ type Registry struct { dimHashesByName map[string]uint64 uncheckedCollectors []Collector pedanticChecksEnabled bool + nameValidationScheme model.ValidationScheme } // Register implements Registerer. @@ -503,6 +518,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, registeredDescIDs, + r.nameValidationScheme, )) case metric, ok := <-umc: if !ok { @@ -513,6 +529,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, nil, + r.nameValidationScheme, )) default: if goroutineBudget <= 0 || len(checkedCollectors)+len(uncheckedCollectors) == 0 { @@ -530,6 +547,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, registeredDescIDs, + r.nameValidationScheme, )) case metric, ok := <-umc: if !ok { @@ -540,6 +558,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) { metric, metricFamiliesByName, metricHashes, nil, + r.nameValidationScheme, )) } break @@ -622,6 +641,7 @@ func processMetric( metricFamiliesByName map[string]*dto.MetricFamily, metricHashes map[uint64]struct{}, registeredDescIDs map[uint64]struct{}, + nameValidationScheme model.ValidationScheme, ) error { desc := metric.Desc() // Wrapped metrics collected by an unchecked Collector can have an @@ -705,7 +725,7 @@ func processMetric( } metricFamiliesByName[desc.fqName] = metricFamily } - if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes); err != nil { + if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes, nameValidationScheme); err != nil { return err } if registeredDescIDs != nil { @@ -791,7 +811,8 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { metricFamiliesByName[mf.GetName()] = existingMF } for _, m := range mf.Metric { - if err := checkMetricConsistency(existingMF, m, metricHashes); err != nil { + // TODO(juliusmh): hardcoded UTF8 validation + if err := checkMetricConsistency(existingMF, m, metricHashes, model.UTF8Validation); err != nil { errs = append(errs, err) continue } @@ -870,6 +891,7 @@ func checkMetricConsistency( metricFamily *dto.MetricFamily, dtoMetric *dto.Metric, metricHashes map[uint64]struct{}, + nameValidationScheme model.ValidationScheme, ) error { name := metricFamily.GetName() @@ -894,7 +916,7 @@ func checkMetricConsistency( name, dtoMetric, labelName, ) } - if !checkLabelName(labelName) { + if !checkLabelName(labelName, nameValidationScheme) { return fmt.Errorf( "collected metric %q { %s} has a label with an invalid name: %s", name, dtoMetric, labelName, diff --git a/prometheus/testutil/lint.go b/prometheus/testutil/lint.go index 8d2f05500..1600ea592 100644 --- a/prometheus/testutil/lint.go +++ b/prometheus/testutil/lint.go @@ -18,23 +18,24 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil/promlint" + "github.com/prometheus/common/model" ) // CollectAndLint registers the provided Collector with a newly created pedantic // Registry. It then calls GatherAndLint with that Registry and with the // provided metricNames. -func CollectAndLint(c prometheus.Collector, metricNames ...string) ([]promlint.Problem, error) { +func CollectAndLint(c prometheus.Collector, validationScheme model.ValidationScheme, metricNames ...string) ([]promlint.Problem, error) { reg := prometheus.NewPedanticRegistry() if err := reg.Register(c); err != nil { return nil, fmt.Errorf("registering collector failed: %w", err) } - return GatherAndLint(reg, metricNames...) + return GatherAndLint(reg, validationScheme, metricNames...) } // GatherAndLint gathers all metrics from the provided Gatherer and checks them // with the linter in the promlint package. If any metricNames are provided, // only metrics with those names are checked. -func GatherAndLint(g prometheus.Gatherer, metricNames ...string) ([]promlint.Problem, error) { +func GatherAndLint(g prometheus.Gatherer, validationScheme model.ValidationScheme, metricNames ...string) ([]promlint.Problem, error) { got, err := g.Gather() if err != nil { return nil, fmt.Errorf("gathering metrics failed: %w", err) @@ -42,5 +43,5 @@ func GatherAndLint(g prometheus.Gatherer, metricNames ...string) ([]promlint.Pro if metricNames != nil { got = filterMetrics(got, metricNames) } - return promlint.NewWithMetricFamilies(got).Lint() + return promlint.NewWithMetricFamilies(got).Lint(validationScheme) } diff --git a/prometheus/testutil/lint_test.go b/prometheus/testutil/lint_test.go index 547465d77..3cec1871d 100644 --- a/prometheus/testutil/lint_test.go +++ b/prometheus/testutil/lint_test.go @@ -17,6 +17,7 @@ import ( "testing" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/model" ) func TestCollectAndLintGood(t *testing.T) { @@ -33,7 +34,7 @@ func TestCollectAndLintGood(t *testing.T) { cnt.WithLabelValues("bar") cnt.WithLabelValues("baz") - problems, err := CollectAndLint(cnt) + problems, err := CollectAndLint(cnt, model.UTF8Validation) if err != nil { t.Error("Unexpected error:", err) } @@ -56,7 +57,7 @@ func TestCollectAndLintBad(t *testing.T) { cnt.WithLabelValues("bar") cnt.WithLabelValues("baz") - problems, err := CollectAndLint(cnt) + problems, err := CollectAndLint(cnt, model.UTF8Validation) if err != nil { t.Error("Unexpected error:", err) } diff --git a/prometheus/testutil/promlint/promlint.go b/prometheus/testutil/promlint/promlint.go index ea46f38ec..519ae0792 100644 --- a/prometheus/testutil/promlint/promlint.go +++ b/prometheus/testutil/promlint/promlint.go @@ -21,6 +21,7 @@ import ( dto "github.com/prometheus/client_model/go" "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" ) // A Linter is a Prometheus metrics linter. It identifies issues with metric @@ -64,7 +65,7 @@ func (l *Linter) AddCustomValidations(vs ...Validation) { // Lint performs a linting pass, returning a slice of Problems indicating any // issues found in the metrics stream. The slice is sorted by metric name // and issue description. -func (l *Linter) Lint() ([]Problem, error) { +func (l *Linter) Lint(nameValidationScheme model.ValidationScheme) ([]Problem, error) { var problems []Problem if l.r != nil { @@ -72,7 +73,7 @@ func (l *Linter) Lint() ([]Problem, error) { mf := &dto.MetricFamily{} for { - if err := d.Decode(mf); err != nil { + if err := d.Decode(mf, nameValidationScheme); err != nil { if errors.Is(err, io.EOF) { break } diff --git a/prometheus/testutil/promlint/promlint_test.go b/prometheus/testutil/promlint/promlint_test.go index c6a4e27a7..b01f56248 100644 --- a/prometheus/testutil/promlint/promlint_test.go +++ b/prometheus/testutil/promlint/promlint_test.go @@ -21,6 +21,7 @@ import ( "testing" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "github.com/prometheus/client_golang/prometheus/testutil/promlint" ) @@ -424,7 +425,7 @@ x_ounces 10 t.Run(tt.name, func(t *testing.T) { l := promlint.New(strings.NewReader(tt.in)) - problems, err := l.Lint() + problems, err := l.Lint(model.UTF8Validation) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -776,7 +777,7 @@ func runTests(t *testing.T, tests []test) { t.Run(tt.name, func(t *testing.T) { l := promlint.New(strings.NewReader(tt.in)) - problems, err := l.Lint() + problems, err := l.Lint(model.UTF8Validation) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -791,7 +792,7 @@ func runTests(t *testing.T, tests []test) { func TestCustomValidations(t *testing.T) { lintAndVerify := func(l *promlint.Linter, cv test) { - problems, err := l.Lint() + problems, err := l.Lint(model.UTF8Validation) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/prometheus/value.go b/prometheus/value.go index cc23011fa..6b9eb2930 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus/client_golang/prometheus/internal" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/model" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -253,7 +254,8 @@ func newExemplar(value float64, ts time.Time, l Labels) (*dto.Exemplar, error) { labelPairs := make([]*dto.LabelPair, 0, len(l)) var runes int for name, value := range l { - if !checkLabelName(name) { + // TODO(juliusmh): hardcoded UTF8 validation + if !checkLabelName(name, model.UTF8Validation) { return nil, fmt.Errorf("exemplar label name %q is invalid", name) } runes += utf8.RuneCountInString(name)