Skip to content

Commit 641e5b7

Browse files
committed
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]>
1 parent 07b1397 commit 641e5b7

13 files changed

+263
-43
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
@@ -54,9 +54,9 @@ type Desc struct {
5454
// constLabelPairs contains precalculated DTO label pairs based on
5555
// the constant labels.
5656
constLabelPairs []*dto.LabelPair
57-
// variableLabels contains names of labels for which the metric
58-
// maintains variable values.
59-
variableLabels []string
57+
// variableLabels contains names of labels and normalization function for
58+
// which the metric maintains variable values.
59+
variableLabels ConstrainedLabels
6060
// id is a hash of the values of the ConstLabels and fqName. This
6161
// must be unique among all registered descriptors and can therefore be
6262
// used as an identifier of the descriptor.
@@ -80,10 +80,24 @@ type Desc struct {
8080
// For constLabels, the label values are constant. Therefore, they are fully
8181
// specified in the Desc. See the Collector example for a usage pattern.
8282
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
83+
return V2.NewDesc(fqName, help, UnconstrainedLabels(variableLabels), constLabels)
84+
}
85+
86+
// NewDesc allocates and initializes a new Desc. Errors are recorded in the Desc
87+
// and will be reported on registration time. variableLabels and constLabels can
88+
// be nil if no such labels should be set. fqName must not be empty.
89+
//
90+
// variableLabels only contain the label names and normalization functions. Their
91+
// label values are variable and therefore not part of the Desc. (They are managed
92+
// within the Metric.)
93+
//
94+
// For constLabels, the label values are constant. Therefore, they are fully
95+
// specified in the Desc. See the Collector example for a usage pattern.
96+
func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, constLabels Labels) *Desc {
8397
d := &Desc{
8498
fqName: fqName,
8599
help: help,
86-
variableLabels: variableLabels,
100+
variableLabels: variableLabels.constrainedLabels(),
87101
}
88102
if !model.IsValidMetricName(model.LabelValue(fqName)) {
89103
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
@@ -93,7 +107,7 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
93107
// their sorted label names) plus the fqName (at position 0).
94108
labelValues := make([]string, 1, len(constLabels)+1)
95109
labelValues[0] = fqName
96-
labelNames := make([]string, 0, len(constLabels)+len(variableLabels))
110+
labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels))
97111
labelNameSet := map[string]struct{}{}
98112
// First add only the const label names and sort them...
99113
for labelName := range constLabels {
@@ -118,13 +132,13 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
118132
// Now add the variable label names, but prefix them with something that
119133
// cannot be in a regular label name. That prevents matching the label
120134
// dimension with a different mix between preset and variable labels.
121-
for _, labelName := range variableLabels {
122-
if !checkLabelName(labelName) {
123-
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName)
135+
for _, label := range d.variableLabels {
136+
if !checkLabelName(label.Name) {
137+
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label.Name, fqName)
124138
return d
125139
}
126-
labelNames = append(labelNames, "$"+labelName)
127-
labelNameSet[labelName] = struct{}{}
140+
labelNames = append(labelNames, "$"+label.Name)
141+
labelNameSet[label.Name] = struct{}{}
128142
}
129143
if len(labelNames) != len(labelNameSet) {
130144
d.err = errors.New("duplicate label names")

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)