Skip to content

Commit 5a063ae

Browse files
committed
feat(prometheus): explicit label/metric name validation scheme
1 parent 2dd24f2 commit 5a063ae

File tree

7 files changed

+76
-33
lines changed

7 files changed

+76
-33
lines changed

prometheus/desc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
9595
help: help,
9696
variableLabels: variableLabels.compile(),
9797
}
98-
if !model.IsValidMetricName(model.LabelValue(fqName)) {
98+
if !model.IsValidMetricName(model.LabelValue(fqName), model.UTF8Validation) {
9999
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
100100
return d
101101
}
@@ -107,7 +107,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
107107
labelNameSet := map[string]struct{}{}
108108
// First add only the const label names and sort them...
109109
for labelName := range constLabels {
110-
if !checkLabelName(labelName) {
110+
if !checkLabelName(labelName, model.UTF8Validation) {
111111
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName)
112112
return d
113113
}
@@ -129,7 +129,7 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
129129
// cannot be in a regular label name. That prevents matching the label
130130
// dimension with a different mix between preset and variable labels.
131131
for _, label := range d.variableLabels.names {
132-
if !checkLabelName(label) {
132+
if !checkLabelName(label, model.UTF8Validation) {
133133
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label, fqName)
134134
return d
135135
}

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
}

prometheus/registry.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/cespare/xxhash/v2"
3232
dto "github.com/prometheus/client_model/go"
3333
"github.com/prometheus/common/expfmt"
34+
"github.com/prometheus/common/model"
3435
"google.golang.org/protobuf/proto"
3536
)
3637

@@ -62,14 +63,27 @@ func init() {
6263
MustRegister(NewGoCollector())
6364
}
6465

66+
type RegistryOption func(*Registry)
67+
68+
func WithNameValidationScheme(scheme model.ValidationScheme) RegistryOption {
69+
return func(r *Registry) {
70+
r.nameValidationScheme = scheme
71+
}
72+
}
73+
6574
// NewRegistry creates a new vanilla Registry without any Collectors
6675
// pre-registered.
67-
func NewRegistry() *Registry {
68-
return &Registry{
69-
collectorsByID: map[uint64]Collector{},
70-
descIDs: map[uint64]struct{}{},
71-
dimHashesByName: map[string]uint64{},
76+
func NewRegistry(opts ...RegistryOption) *Registry {
77+
reg := &Registry{
78+
collectorsByID: map[uint64]Collector{},
79+
descIDs: map[uint64]struct{}{},
80+
dimHashesByName: map[string]uint64{},
81+
nameValidationScheme: model.UTF8Validation,
82+
}
83+
for _, opt := range opts {
84+
opt(reg)
7285
}
86+
return reg
7387
}
7488

7589
// NewPedanticRegistry returns a registry that checks during collection if each
@@ -264,6 +278,7 @@ type Registry struct {
264278
dimHashesByName map[string]uint64
265279
uncheckedCollectors []Collector
266280
pedanticChecksEnabled bool
281+
nameValidationScheme model.ValidationScheme
267282
}
268283

269284
// Register implements Registerer.
@@ -503,6 +518,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
503518
metric, metricFamiliesByName,
504519
metricHashes,
505520
registeredDescIDs,
521+
r.nameValidationScheme,
506522
))
507523
case metric, ok := <-umc:
508524
if !ok {
@@ -513,6 +529,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
513529
metric, metricFamiliesByName,
514530
metricHashes,
515531
nil,
532+
r.nameValidationScheme,
516533
))
517534
default:
518535
if goroutineBudget <= 0 || len(checkedCollectors)+len(uncheckedCollectors) == 0 {
@@ -530,6 +547,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
530547
metric, metricFamiliesByName,
531548
metricHashes,
532549
registeredDescIDs,
550+
r.nameValidationScheme,
533551
))
534552
case metric, ok := <-umc:
535553
if !ok {
@@ -540,6 +558,7 @@ func (r *Registry) Gather() ([]*dto.MetricFamily, error) {
540558
metric, metricFamiliesByName,
541559
metricHashes,
542560
nil,
561+
r.nameValidationScheme,
543562
))
544563
}
545564
break
@@ -622,6 +641,7 @@ func processMetric(
622641
metricFamiliesByName map[string]*dto.MetricFamily,
623642
metricHashes map[uint64]struct{},
624643
registeredDescIDs map[uint64]struct{},
644+
nameValidationScheme model.ValidationScheme,
625645
) error {
626646
desc := metric.Desc()
627647
// Wrapped metrics collected by an unchecked Collector can have an
@@ -705,7 +725,7 @@ func processMetric(
705725
}
706726
metricFamiliesByName[desc.fqName] = metricFamily
707727
}
708-
if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes); err != nil {
728+
if err := checkMetricConsistency(metricFamily, dtoMetric, metricHashes, nameValidationScheme); err != nil {
709729
return err
710730
}
711731
if registeredDescIDs != nil {
@@ -791,7 +811,8 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) {
791811
metricFamiliesByName[mf.GetName()] = existingMF
792812
}
793813
for _, m := range mf.Metric {
794-
if err := checkMetricConsistency(existingMF, m, metricHashes); err != nil {
814+
// TODO(juliusmh): hardcoded UTF8 validation
815+
if err := checkMetricConsistency(existingMF, m, metricHashes, model.UTF8Validation); err != nil {
795816
errs = append(errs, err)
796817
continue
797818
}
@@ -870,6 +891,7 @@ func checkMetricConsistency(
870891
metricFamily *dto.MetricFamily,
871892
dtoMetric *dto.Metric,
872893
metricHashes map[uint64]struct{},
894+
nameValidationScheme model.ValidationScheme,
873895
) error {
874896
name := metricFamily.GetName()
875897

@@ -894,7 +916,7 @@ func checkMetricConsistency(
894916
name, dtoMetric, labelName,
895917
)
896918
}
897-
if !checkLabelName(labelName) {
919+
if !checkLabelName(labelName, nameValidationScheme) {
898920
return fmt.Errorf(
899921
"collected metric %q { %s} has a label with an invalid name: %s",
900922
name, dtoMetric, labelName,

prometheus/testutil/lint.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,30 @@ import (
1818

1919
"github.com/prometheus/client_golang/prometheus"
2020
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
21+
"github.com/prometheus/common/model"
2122
)
2223

2324
// CollectAndLint registers the provided Collector with a newly created pedantic
2425
// Registry. It then calls GatherAndLint with that Registry and with the
2526
// provided metricNames.
26-
func CollectAndLint(c prometheus.Collector, metricNames ...string) ([]promlint.Problem, error) {
27+
func CollectAndLint(c prometheus.Collector, validationScheme model.ValidationScheme, metricNames ...string) ([]promlint.Problem, error) {
2728
reg := prometheus.NewPedanticRegistry()
2829
if err := reg.Register(c); err != nil {
2930
return nil, fmt.Errorf("registering collector failed: %w", err)
3031
}
31-
return GatherAndLint(reg, metricNames...)
32+
return GatherAndLint(reg, validationScheme, metricNames...)
3233
}
3334

3435
// GatherAndLint gathers all metrics from the provided Gatherer and checks them
3536
// with the linter in the promlint package. If any metricNames are provided,
3637
// only metrics with those names are checked.
37-
func GatherAndLint(g prometheus.Gatherer, metricNames ...string) ([]promlint.Problem, error) {
38+
func GatherAndLint(g prometheus.Gatherer, validationScheme model.ValidationScheme, metricNames ...string) ([]promlint.Problem, error) {
3839
got, err := g.Gather()
3940
if err != nil {
4041
return nil, fmt.Errorf("gathering metrics failed: %w", err)
4142
}
4243
if metricNames != nil {
4344
got = filterMetrics(got, metricNames)
4445
}
45-
return promlint.NewWithMetricFamilies(got).Lint()
46+
return promlint.NewWithMetricFamilies(got).Lint(validationScheme)
4647
}

prometheus/testutil/promlint/promlint.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
dto "github.com/prometheus/client_model/go"
2323
"github.com/prometheus/common/expfmt"
24+
"github.com/prometheus/common/model"
2425
)
2526

2627
// A Linter is a Prometheus metrics linter. It identifies issues with metric
@@ -64,15 +65,15 @@ func (l *Linter) AddCustomValidations(vs ...Validation) {
6465
// Lint performs a linting pass, returning a slice of Problems indicating any
6566
// issues found in the metrics stream. The slice is sorted by metric name
6667
// and issue description.
67-
func (l *Linter) Lint() ([]Problem, error) {
68+
func (l *Linter) Lint(nameValidationScheme model.ValidationScheme) ([]Problem, error) {
6869
var problems []Problem
6970

7071
if l.r != nil {
7172
d := expfmt.NewDecoder(l.r, expfmt.NewFormat(expfmt.TypeTextPlain))
7273

7374
mf := &dto.MetricFamily{}
7475
for {
75-
if err := d.Decode(mf); err != nil {
76+
if err := d.Decode(mf, nameValidationScheme); err != nil {
7677
if errors.Is(err, io.EOF) {
7778
break
7879
}

prometheus/value.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/prometheus/client_golang/prometheus/internal"
2424

2525
dto "github.com/prometheus/client_model/go"
26+
"github.com/prometheus/common/model"
2627
"google.golang.org/protobuf/proto"
2728
"google.golang.org/protobuf/types/known/timestamppb"
2829
)
@@ -253,7 +254,8 @@ func newExemplar(value float64, ts time.Time, l Labels) (*dto.Exemplar, error) {
253254
labelPairs := make([]*dto.LabelPair, 0, len(l))
254255
var runes int
255256
for name, value := range l {
256-
if !checkLabelName(name) {
257+
// TODO(juliusmh): hardcoded UTF8 validation
258+
if !checkLabelName(name, model.UTF8Validation) {
257259
return nil, fmt.Errorf("exemplar label name %q is invalid", name)
258260
}
259261
runes += utf8.RuneCountInString(name)

0 commit comments

Comments
 (0)