Skip to content

Commit ddc5c35

Browse files
juliusmhaknuds1
authored andcommitted
feat(prometheus): explicit label/metric name validation scheme
Signed-off-by: Arve Knudsen <[email protected]>
1 parent db4db7b commit ddc5c35

File tree

12 files changed

+202
-70
lines changed

12 files changed

+202
-70
lines changed

go.mod

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ module github.com/prometheus/client_golang
22

33
go 1.23.0
44

5+
toolchain go1.24.4
6+
7+
toolchain go1.24.4
8+
59
require (
610
github.com/beorn7/perks v1.0.1
711
github.com/cespare/xxhash/v2 v2.3.0
@@ -10,7 +14,7 @@ require (
1014
github.com/klauspost/compress v1.18.0
1115
github.com/kylelemons/godebug v1.1.0
1216
github.com/prometheus/client_model v0.6.2
13-
github.com/prometheus/common v0.65.0
17+
github.com/prometheus/common v0.65.1-0.20250704063051-dd764ff5ada2
1418
github.com/prometheus/procfs v0.16.1
1519
go.uber.org/goleak v1.3.0
1620
golang.org/x/sys v0.33.0
@@ -24,9 +28,9 @@ require (
2428
github.com/modern-go/reflect2 v1.0.2 // indirect
2529
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
2630
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
27-
golang.org/x/net v0.40.0 // indirect
31+
golang.org/x/net v0.41.0 // indirect
2832
golang.org/x/oauth2 v0.30.0 // indirect
29-
golang.org/x/text v0.25.0 // indirect
33+
golang.org/x/text v0.26.0 // indirect
3034
gopkg.in/yaml.v2 v2.4.0 // indirect
3135
)
3236

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
3535
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3636
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
3737
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
38-
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
39-
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
38+
github.com/prometheus/common v0.65.1-0.20250704063051-dd764ff5ada2 h1:+PRxWMSLkDSEsjjztnLj3Dp009ayxU2LWYI8uX2caXU=
39+
github.com/prometheus/common v0.65.1-0.20250704063051-dd764ff5ada2/go.mod h1:LL3lcZII3UXGO4InbF+BTSsiAAPUBnwFVbp4gBWIMqw=
4040
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
4141
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
4242
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -48,14 +48,14 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
4848
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
4949
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
5050
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
51-
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
52-
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
51+
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
52+
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
5353
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
5454
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
5555
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
5656
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
57-
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
58-
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
57+
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
58+
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
5959
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
6060
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
6161
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

prometheus/desc.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,32 @@ type Desc struct {
6666
err error
6767
}
6868

69+
type descriptorOptions struct {
70+
validationScheme model.ValidationScheme
71+
}
72+
73+
// newDescriptorOptions creates default descriptor options and applies opts.
74+
func newDescriptorOptions(opts ...DescOption) *descriptorOptions {
75+
d := &descriptorOptions{
76+
validationScheme: model.UTF8Validation,
77+
}
78+
for _, o := range opts {
79+
o(d)
80+
}
81+
return d
82+
}
83+
84+
// WithValidationScheme ensures descriptor's label and metric names adhere to scheme.
85+
// Default is UTF-8 validation.
86+
func WithValidationScheme(scheme model.ValidationScheme) DescOption {
87+
return func(o *descriptorOptions) {
88+
o.validationScheme = scheme
89+
}
90+
}
91+
92+
// DescOption are options that can be passed to NewDesc
93+
type DescOption func(*descriptorOptions)
94+
6995
// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc
7096
// and will be reported on registration time. variableLabels and constLabels can
7197
// be nil if no such labels should be set. fqName must not be empty.
@@ -75,8 +101,8 @@ type Desc struct {
75101
//
76102
// For constLabels, the label values are constant. Therefore, they are fully
77103
// specified in the Desc. See the Collector example for a usage pattern.
78-
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
79-
return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels)
104+
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels, opts ...DescOption) *Desc {
105+
return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels, opts...)
80106
}
81107

82108
// 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) *
89115
//
90116
// For constLabels, the label values are constant. Therefore, they are fully
91117
// specified in the Desc. See the Collector example for a usage pattern.
92-
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc {
118+
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels, opts ...DescOption) *Desc {
93119
d := &Desc{
94120
fqName: fqName,
95121
help: help,
96122
variableLabels: variableLabels.compile(),
97123
}
98-
if !model.IsValidMetricName(model.LabelValue(fqName)) {
124+
descOpts := newDescriptorOptions(opts...)
125+
if !model.IsValidMetricName(model.LabelValue(fqName), descOpts.validationScheme) {
99126
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
100127
return d
101128
}
@@ -107,7 +134,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
107134
labelNameSet := map[string]struct{}{}
108135
// First add only the const label names and sort them...
109136
for labelName := range constLabels {
110-
if !checkLabelName(labelName) {
137+
if !checkLabelName(labelName, descOpts.validationScheme) {
111138
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName)
112139
return d
113140
}
@@ -129,7 +156,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
129156
// cannot be in a regular label name. That prevents matching the label
130157
// dimension with a different mix between preset and variable labels.
131158
for _, label := range d.variableLabels.names {
132-
if !checkLabelName(label) {
159+
if !checkLabelName(label, descOpts.validationScheme) {
133160
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName)
134161
return d
135162
}

prometheus/desc_test.go

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,86 @@ package prometheus
1515

1616
import (
1717
"testing"
18+
19+
"github.com/prometheus/common/model"
1820
)
1921

20-
func TestNewDescInvalidLabelValues(t *testing.T) {
21-
desc := NewDesc(
22-
"sample_label",
23-
"sample label",
24-
nil,
25-
Labels{"a": "\xFF"},
26-
)
27-
if desc.err == nil {
28-
t.Errorf("NewDesc: expected error because: %s", desc.err)
22+
func TestNewDesc(t *testing.T) {
23+
testCases := []struct {
24+
name string
25+
fqName string
26+
help string
27+
variableLabels []string
28+
labels Labels
29+
opts []DescOption
30+
wantErr string
31+
}{
32+
{
33+
name: "invalid label value",
34+
fqName: "sample_label",
35+
help: "sample label",
36+
variableLabels: nil,
37+
labels: Labels{"a": "\xff"},
38+
wantErr: `label value "\xff" is not valid UTF-8`,
39+
},
40+
{
41+
name: "nil label values",
42+
fqName: "sample_label",
43+
help: "sample label",
44+
variableLabels: nil,
45+
labels: nil,
46+
},
47+
{
48+
name: "invalid label name",
49+
fqName: "sample_label",
50+
help: "sample label",
51+
variableLabels: nil,
52+
labels: Labels{"\xff": "test"},
53+
wantErr: `"\xff" is not a valid label name for metric "sample_label"`,
54+
},
55+
{
56+
name: "invalid legacy label name",
57+
fqName: "sample_label",
58+
help: "sample label",
59+
variableLabels: nil,
60+
labels: Labels{"test😀": "test"},
61+
opts: []DescOption{WithValidationScheme(model.LegacyValidation)},
62+
wantErr: `"test😀" is not a valid label name for metric "sample_label"`,
63+
},
64+
{
65+
name: "invalid legacy metric name",
66+
fqName: "sample_label😀",
67+
help: "sample label",
68+
opts: []DescOption{WithValidationScheme(model.LegacyValidation)},
69+
wantErr: `"sample_label😀" is not a valid metric name`,
70+
},
71+
{
72+
name: "valid utf8 label name",
73+
fqName: "sample_label",
74+
help: "sample label",
75+
variableLabels: nil,
76+
labels: Labels{"test😀": "test"},
77+
},
2978
}
30-
}
31-
32-
func TestNewDescNilLabelValues(t *testing.T) {
33-
desc := NewDesc(
34-
"sample_label",
35-
"sample label",
36-
nil,
37-
nil,
38-
)
39-
if desc.err != nil {
40-
t.Errorf("NewDesc: unexpected error: %s", desc.err)
79+
for _, tc := range testCases {
80+
t.Run(tc.name, func(t *testing.T) {
81+
desc := NewDesc(
82+
tc.fqName,
83+
tc.help,
84+
tc.variableLabels,
85+
tc.labels,
86+
tc.opts...,
87+
)
88+
if desc.err != nil && tc.wantErr != desc.err.Error() {
89+
t.Fatalf("NewDesc: expected error %q but got %+v", tc.wantErr, desc.err)
90+
} else if desc.err == nil && tc.wantErr != "" {
91+
t.Fatalf("NewDesc: expected error %q but got nil", tc.wantErr)
92+
} else if desc.err != nil && tc.wantErr == "" {
93+
t.Fatalf("NewDesc: %+v", desc.err)
94+
}
95+
})
4196
}
97+
4298
}
4399

44100
func TestNewDescWithNilLabelValues_String(t *testing.T) {

prometheus/labels.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,6 @@ func validateLabelValues(vals []string, expectedNumberOfValues int) error {
183183
return nil
184184
}
185185

186-
func checkLabelName(l string) bool {
187-
return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix)
186+
func checkLabelName(l string, nameValidationSheme model.ValidationScheme) bool {
187+
return model.LabelName(l).IsValid(nameValidationSheme) && !strings.HasPrefix(l, reservedLabelPrefix)
188188
}

prometheus/push/push.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ type HTTPDoer interface {
7070
type Pusher struct {
7171
error error
7272

73-
url, job string
74-
grouping map[string]string
73+
url, job string
74+
grouping map[string]string
75+
validationScheme model.ValidationScheme
7576

7677
gatherers prometheus.Gatherers
7778
registerer prometheus.Registerer
@@ -84,11 +85,22 @@ type Pusher struct {
8485
expfmt expfmt.Format
8586
}
8687

88+
// Option used to create Pusher instances.
89+
type Option func(*Pusher)
90+
91+
// WithValidationScheme sets the validation used for label and metric names.
92+
// Default is model.UTF8Validation.
93+
func WithValidationScheme(scheme model.ValidationScheme) Option {
94+
return func(p *Pusher) {
95+
p.validationScheme = scheme
96+
}
97+
}
98+
8799
// New creates a new Pusher to push to the provided URL with the provided job
88100
// name (which must not be empty). You can use just host:port or ip:port as url,
89101
// in which case “http://” is added automatically. Alternatively, include the
90102
// schema in the URL. However, do not include the “/metrics/jobs/…” part.
91-
func New(url, job string) *Pusher {
103+
func New(url, job string, opts ...Option) *Pusher {
92104
var (
93105
reg = prometheus.NewRegistry()
94106
err error
@@ -101,16 +113,21 @@ func New(url, job string) *Pusher {
101113
}
102114
url = strings.TrimSuffix(url, "/")
103115

104-
return &Pusher{
105-
error: err,
106-
url: url,
107-
job: job,
108-
grouping: map[string]string{},
109-
gatherers: prometheus.Gatherers{reg},
110-
registerer: reg,
111-
client: &http.Client{},
112-
expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim),
116+
pusher := &Pusher{
117+
error: err,
118+
url: url,
119+
job: job,
120+
grouping: map[string]string{},
121+
gatherers: prometheus.Gatherers{reg},
122+
registerer: reg,
123+
client: &http.Client{},
124+
expfmt: expfmt.NewFormat(expfmt.TypeProtoDelim),
125+
validationScheme: model.UTF8Validation,
126+
}
127+
for _, opt := range opts {
128+
opt(pusher)
113129
}
130+
return pusher
114131
}
115132

116133
// Push collects/gathers all metrics from all Collectors and Gatherers added to
@@ -182,7 +199,7 @@ func (p *Pusher) Error() error {
182199
// For convenience, this method returns a pointer to the Pusher itself.
183200
func (p *Pusher) Grouping(name, value string) *Pusher {
184201
if p.error == nil {
185-
if !model.LabelName(name).IsValid() {
202+
if !model.LabelName(name).IsValid(p.validationScheme) {
186203
p.error = fmt.Errorf("grouping label has invalid name: %s", name)
187204
return p
188205
}

0 commit comments

Comments
 (0)