Skip to content

feat(prometheus): explicit label/metric name validation scheme #1822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: release-1.22
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 (
Expand All @@ -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
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
39 changes: 33 additions & 6 deletions prometheus/desc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
96 changes: 76 additions & 20 deletions prometheus/desc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions prometheus/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
43 changes: 30 additions & 13 deletions prometheus/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Loading
Loading