Skip to content

Commit fae2f63

Browse files
authored
Add constrained labels and Constrained variant for all MetricVecs (#1151)
* Introduce MetricVecOpts and add constraints to VariableLabels MetricVecOpts exposes options specific to MetricVec initialisation. The first option exposed by MetricVecOpts are constraints on VariableLabels, allowing restrictions on the possible values a label can take, to prevent cardinality explosion when the label value comes from a non-trusted source (as a user input or HTTP header). Signed-off-by: Quentin Devos <[email protected]> * Add tests Signed-off-by: Quentin Devos <[email protected]> Signed-off-by: Quentin Devos <[email protected]>
1 parent 3d765a1 commit fae2f63

14 files changed

+626
-44
lines changed

prometheus/counter.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ type ExemplarAdder interface {
5959
// CounterOpts is an alias for Opts. See there for doc comments.
6060
type CounterOpts Opts
6161

62+
// CounterVecOpts bundles the options to create a CounterVec metric.
63+
// It is mandatory to set CounterOpts, see there for mandatory fields. VariableLabels
64+
// is optional and can safely be left to its default value.
65+
type CounterVecOpts struct {
66+
CounterOpts
67+
68+
// VariableLabels are used to partition the metric vector by the given set
69+
// of labels. Each label value will be constrained with the optional Contraint
70+
// function, if provided.
71+
VariableLabels ConstrainableLabels
72+
}
73+
6274
// NewCounter creates a new Counter based on the provided CounterOpts.
6375
//
6476
// The returned implementation also implements ExemplarAdder. It is safe to
@@ -174,16 +186,24 @@ type CounterVec struct {
174186
// NewCounterVec creates a new CounterVec based on the provided CounterOpts and
175187
// partitioned by the given label names.
176188
func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
177-
desc := NewDesc(
189+
return V2.NewCounterVec(CounterVecOpts{
190+
CounterOpts: opts,
191+
VariableLabels: UnconstrainedLabels(labelNames),
192+
})
193+
}
194+
195+
// NewCounterVec creates a new CounterVec based on the provided CounterVecOpts.
196+
func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
197+
desc := V2.NewDesc(
178198
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
179199
opts.Help,
180-
labelNames,
200+
opts.VariableLabels,
181201
opts.ConstLabels,
182202
)
183203
return &CounterVec{
184204
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
185205
if len(lvs) != len(desc.variableLabels) {
186-
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs))
206+
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), lvs))
187207
}
188208
result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now}
189209
result.init(result) // Init self-collection.

prometheus/counter_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func TestCounterVecGetMetricWithInvalidLabelValues(t *testing.T) {
102102
Name: "test",
103103
}, []string{"a"})
104104

105-
labelValues := make([]string, len(test.labels))
105+
labelValues := make([]string, 0, len(test.labels))
106106
for _, val := range test.labels {
107107
labelValues = append(labelValues, val)
108108
}

prometheus/desc.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ type Desc struct {
5353
// constLabelPairs contains precalculated DTO label pairs based on
5454
// the constant labels.
5555
constLabelPairs []*dto.LabelPair
56-
// variableLabels contains names of labels for which the metric
57-
// maintains variable values.
58-
variableLabels []string
56+
// variableLabels contains names of labels and normalization function for
57+
// which the metric maintains variable values.
58+
variableLabels ConstrainedLabels
5959
// id is a hash of the values of the ConstLabels and fqName. This
6060
// must be unique among all registered descriptors and can therefore be
6161
// used as an identifier of the descriptor.
@@ -79,10 +79,24 @@ type Desc struct {
7979
// For constLabels, the label values are constant. Therefore, they are fully
8080
// specified in the Desc. See the Collector example for a usage pattern.
8181
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
82+
return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels)
83+
}
84+
85+
// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc
86+
// and will be reported on registration time. variableLabels and constLabels can
87+
// be nil if no such labels should be set. fqName must not be empty.
88+
//
89+
// variableLabels only contain the label names and normalization functions. Their
90+
// label values are variable and therefore not part of the Desc. (They are managed
91+
// within the Metric.)
92+
//
93+
// For constLabels, the label values are constant. Therefore, they are fully
94+
// specified in the Desc. See the Collector example for a usage pattern.
95+
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc {
8296
d := &Desc{
8397
fqName: fqName,
8498
help: help,
85-
variableLabels: variableLabels,
99+
variableLabels: variableLabels.constrainedLabels(),
86100
}
87101
if !model.IsValidMetricName(model.LabelValue(fqName)) {
88102
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
@@ -92,7 +106,7 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
92106
// their sorted label names) plus the fqName (at position 0).
93107
labelValues := make([]string, 1, len(constLabels)+1)
94108
labelValues[0] = fqName
95-
labelNames := make([]string, 0, len(constLabels)+len(variableLabels))
109+
labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels))
96110
labelNameSet := map[string]struct{}{}
97111
// First add only the const label names and sort them...
98112
for labelName := range constLabels {
@@ -117,13 +131,13 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
117131
// Now add the variable label names, but prefix them with something that
118132
// cannot be in a regular label name. That prevents matching the label
119133
// dimension with a different mix between preset and variable labels.
120-
for _, labelName := range variableLabels {
121-
if !checkLabelName(labelName) {
122-
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName)
134+
for _, label := range d.variableLabels {
135+
if !checkLabelName(label.Name) {
136+
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label.Name, fqName)
123137
return d
124138
}
125-
labelNames = append(labelNames, "$"+labelName)
126-
labelNameSet[labelName] = struct{}{}
139+
labelNames = append(labelNames, "$"+label.Name)
140+
labelNameSet[label.Name] = struct{}{}
127141
}
128142
if len(labelNames) != len(labelNameSet) {
129143
d.err = fmt.Errorf("duplicate label names in constant and variable labels for metric %q", fqName)

prometheus/examples_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,9 @@ func ExampleRegister() {
294294

295295
// Output:
296296
// taskCounter registered.
297-
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [worker_id]} has different label names or a different help string
297+
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [{worker_id <nil>}]} has different label names or a different help string
298298
// taskCounter unregistered.
299-
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [worker_id]} has different label names or a different help string
299+
// taskCounterVec not registered: a previously registered descriptor with the same fully-qualified name as Desc{fqName: "worker_pool_completed_tasks_total", help: "Total number of tasks completed.", constLabels: {}, variableLabels: [{worker_id <nil>}]} has different label names or a different help string
300300
// taskCounterVec registered.
301301
// Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"}
302302
// notMyCounter is nil.

prometheus/gauge.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ type Gauge interface {
5555
// GaugeOpts is an alias for Opts. See there for doc comments.
5656
type GaugeOpts Opts
5757

58+
// GaugeVecOpts bundles the options to create a GaugeVec metric.
59+
// It is mandatory to set GaugeOpts, see there for mandatory fields. VariableLabels
60+
// is optional and can safely be left to its default value.
61+
type GaugeVecOpts struct {
62+
GaugeOpts
63+
64+
// VariableLabels are used to partition the metric vector by the given set
65+
// of labels. Each label value will be constrained with the optional Contraint
66+
// function, if provided.
67+
VariableLabels ConstrainableLabels
68+
}
69+
5870
// NewGauge creates a new Gauge based on the provided GaugeOpts.
5971
//
6072
// The returned implementation is optimized for a fast Set method. If you have a
@@ -138,16 +150,24 @@ type GaugeVec struct {
138150
// NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and
139151
// partitioned by the given label names.
140152
func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
141-
desc := NewDesc(
153+
return V2.NewGaugeVec(GaugeVecOpts{
154+
GaugeOpts: opts,
155+
VariableLabels: UnconstrainedLabels(labelNames),
156+
})
157+
}
158+
159+
// NewGaugeVec creates a new GaugeVec based on the provided GaugeVecOpts.
160+
func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec {
161+
desc := V2.NewDesc(
142162
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
143163
opts.Help,
144-
labelNames,
164+
opts.VariableLabels,
145165
opts.ConstLabels,
146166
)
147167
return &GaugeVec{
148168
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
149169
if len(lvs) != len(desc.variableLabels) {
150-
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs))
170+
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), lvs))
151171
}
152172
result := &gauge{desc: desc, labelPairs: MakeLabelPairs(desc, lvs)}
153173
result.init(result) // Init self-collection.

prometheus/histogram.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,18 @@ type HistogramOpts struct {
469469
NativeHistogramMaxZeroThreshold float64
470470
}
471471

472+
// HistogramVecOpts bundles the options to create a HistogramVec metric.
473+
// It is mandatory to set HistogramOpts, see there for mandatory fields. VariableLabels
474+
// is optional and can safely be left to its default value.
475+
type HistogramVecOpts struct {
476+
HistogramOpts
477+
478+
// VariableLabels are used to partition the metric vector by the given set
479+
// of labels. Each label value will be constrained with the optional Contraint
480+
// function, if provided.
481+
VariableLabels ConstrainableLabels
482+
}
483+
472484
// NewHistogram creates a new Histogram based on the provided HistogramOpts. It
473485
// panics if the buckets in HistogramOpts are not in strictly increasing order.
474486
//
@@ -489,11 +501,11 @@ func NewHistogram(opts HistogramOpts) Histogram {
489501

490502
func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogram {
491503
if len(desc.variableLabels) != len(labelValues) {
492-
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, labelValues))
504+
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), labelValues))
493505
}
494506

495507
for _, n := range desc.variableLabels {
496-
if n == bucketLabel {
508+
if n.Name == bucketLabel {
497509
panic(errBucketLabelNotAllowed)
498510
}
499511
}
@@ -1030,15 +1042,23 @@ type HistogramVec struct {
10301042
// NewHistogramVec creates a new HistogramVec based on the provided HistogramOpts and
10311043
// partitioned by the given label names.
10321044
func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
1033-
desc := NewDesc(
1045+
return V2.NewHistogramVec(HistogramVecOpts{
1046+
HistogramOpts: opts,
1047+
VariableLabels: UnconstrainedLabels(labelNames),
1048+
})
1049+
}
1050+
1051+
// NewHistogramVec creates a new HistogramVec based on the provided HistogramVecOpts.
1052+
func (v2) NewHistogramVec(opts HistogramVecOpts) *HistogramVec {
1053+
desc := V2.NewDesc(
10341054
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
10351055
opts.Help,
1036-
labelNames,
1056+
opts.VariableLabels,
10371057
opts.ConstLabels,
10381058
)
10391059
return &HistogramVec{
10401060
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
1041-
return newHistogram(desc, opts, lvs...)
1061+
return newHistogram(desc, opts.HistogramOpts, lvs...)
10421062
}),
10431063
}
10441064
}

prometheus/labels.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,78 @@ import (
3232
// create a Desc.
3333
type Labels map[string]string
3434

35+
// ConstrainedLabels represents a label name and its constrain function
36+
// to normalize label values. This type is commonly used when constructing
37+
// metric vector Collectors.
38+
type ConstrainedLabel struct {
39+
Name string
40+
Constraint func(string) string
41+
}
42+
43+
func (cl ConstrainedLabel) Constrain(v string) string {
44+
if cl.Constraint == nil {
45+
return v
46+
}
47+
return cl.Constraint(v)
48+
}
49+
50+
// ConstrainableLabels is an interface that allows creating of labels that can
51+
// be optionally constrained.
52+
//
53+
// prometheus.V2().NewCounterVec(CounterVecOpts{
54+
// CounterOpts: {...}, // Usual CounterOpts fields
55+
// VariableLabels: []ConstrainedLabels{
56+
// {Name: "A"},
57+
// {Name: "B", Constraint: func(v string) string { ... }},
58+
// },
59+
// })
60+
type ConstrainableLabels interface {
61+
constrainedLabels() ConstrainedLabels
62+
labelNames() []string
63+
}
64+
65+
// ConstrainedLabels represents a collection of label name -> constrain function
66+
// to normalize label values. This type is commonly used when constructing
67+
// metric vector Collectors.
68+
type ConstrainedLabels []ConstrainedLabel
69+
70+
func (cls ConstrainedLabels) constrainedLabels() ConstrainedLabels {
71+
return cls
72+
}
73+
74+
func (cls ConstrainedLabels) labelNames() []string {
75+
names := make([]string, len(cls))
76+
for i, label := range cls {
77+
names[i] = label.Name
78+
}
79+
return names
80+
}
81+
82+
// UnconstrainedLabels represents collection of label without any constraint on
83+
// their value. Thus, it is simply a collection of label names.
84+
//
85+
// UnconstrainedLabels([]string{ "A", "B" })
86+
//
87+
// is equivalent to
88+
//
89+
// ConstrainedLabels {
90+
// { Name: "A" },
91+
// { Name: "B" },
92+
// }
93+
type UnconstrainedLabels []string
94+
95+
func (uls UnconstrainedLabels) constrainedLabels() ConstrainedLabels {
96+
constrainedLabels := make([]ConstrainedLabel, len(uls))
97+
for i, l := range uls {
98+
constrainedLabels[i] = ConstrainedLabel{Name: l}
99+
}
100+
return constrainedLabels
101+
}
102+
103+
func (uls UnconstrainedLabels) labelNames() []string {
104+
return uls
105+
}
106+
35107
// reservedLabelPrefix is a prefix which is not legal in user-supplied
36108
// label names.
37109
const reservedLabelPrefix = "__"

prometheus/registry.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ func checkDescConsistency(
962962
copy(lpsFromDesc, desc.constLabelPairs)
963963
for _, l := range desc.variableLabels {
964964
lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{
965-
Name: proto.String(l),
965+
Name: proto.String(l.Name),
966966
})
967967
}
968968
if len(lpsFromDesc) != len(dtoMetric.Label) {

prometheus/summary.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ type SummaryOpts struct {
148148
BufCap uint32
149149
}
150150

151+
// SummaryVecOpts bundles the options to create a SummaryVec metric.
152+
// It is mandatory to set SummaryOpts, see there for mandatory fields. VariableLabels
153+
// is optional and can safely be left to its default value.
154+
type SummaryVecOpts struct {
155+
SummaryOpts
156+
157+
// VariableLabels are used to partition the metric vector by the given set
158+
// of labels. Each label value will be constrained with the optional Contraint
159+
// function, if provided.
160+
VariableLabels ConstrainableLabels
161+
}
162+
151163
// Problem with the sliding-window decay algorithm... The Merge method of
152164
// perk/quantile is actually not working as advertised - and it might be
153165
// unfixable, as the underlying algorithm is apparently not capable of merging
@@ -178,11 +190,11 @@ func NewSummary(opts SummaryOpts) Summary {
178190

179191
func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary {
180192
if len(desc.variableLabels) != len(labelValues) {
181-
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, labelValues))
193+
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), labelValues))
182194
}
183195

184196
for _, n := range desc.variableLabels {
185-
if n == quantileLabel {
197+
if n.Name == quantileLabel {
186198
panic(errQuantileLabelNotAllowed)
187199
}
188200
}
@@ -530,20 +542,28 @@ type SummaryVec struct {
530542
// it is handled by the Prometheus server internally, “quantile” is an illegal
531543
// label name. NewSummaryVec will panic if this label name is used.
532544
func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
533-
for _, ln := range labelNames {
545+
return V2.NewSummaryVec(SummaryVecOpts{
546+
SummaryOpts: opts,
547+
VariableLabels: UnconstrainedLabels(labelNames),
548+
})
549+
}
550+
551+
// NewSummaryVec creates a new SummaryVec based on the provided SummaryVecOpts.
552+
func (v2) NewSummaryVec(opts SummaryVecOpts) *SummaryVec {
553+
for _, ln := range opts.VariableLabels.labelNames() {
534554
if ln == quantileLabel {
535555
panic(errQuantileLabelNotAllowed)
536556
}
537557
}
538-
desc := NewDesc(
558+
desc := V2.NewDesc(
539559
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
540560
opts.Help,
541-
labelNames,
561+
opts.VariableLabels,
542562
opts.ConstLabels,
543563
)
544564
return &SummaryVec{
545565
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
546-
return newSummary(desc, opts, lvs...)
566+
return newSummary(desc, opts.SummaryOpts, lvs...)
547567
}),
548568
}
549569
}

0 commit comments

Comments
 (0)