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 <4972091+Okhoshi@users.noreply.github.com>

* Add tests

Signed-off-by: Quentin Devos <4972091+Okhoshi@users.noreply.github.com>

Signed-off-by: Quentin Devos <4972091+Okhoshi@users.noreply.github.com>
This commit is contained in:
Quentin D 2022-12-13 13:47:52 +01:00 committed by GitHub
parent 3d765a161a
commit fae2f6306b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 626 additions and 44 deletions

View File

@ -59,6 +59,18 @@ type ExemplarAdder interface {
// CounterOpts is an alias for Opts. See there for doc comments.
type CounterOpts Opts
// CounterVecOpts bundles the options to create a CounterVec metric.
// It is mandatory to set CounterOpts, see there for mandatory fields. VariableLabels
// is optional and can safely be left to its default value.
type CounterVecOpts struct {
CounterOpts
// VariableLabels are used to partition the metric vector by the given set
// of labels. Each label value will be constrained with the optional Contraint
// function, if provided.
VariableLabels ConstrainableLabels
}
// NewCounter creates a new Counter based on the provided CounterOpts.
//
// The returned implementation also implements ExemplarAdder. It is safe to
@ -174,16 +186,24 @@ type CounterVec struct {
// NewCounterVec creates a new CounterVec based on the provided CounterOpts and
// partitioned by the given label names.
func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
desc := NewDesc(
return V2.NewCounterVec(CounterVecOpts{
CounterOpts: opts,
VariableLabels: UnconstrainedLabels(labelNames),
})
}
// NewCounterVec creates a new CounterVec based on the provided CounterVecOpts.
func (v2) NewCounterVec(opts CounterVecOpts) *CounterVec {
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.VariableLabels,
opts.ConstLabels,
)
return &CounterVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
if len(lvs) != len(desc.variableLabels) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs))
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), lvs))
}
result := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now}
result.init(result) // Init self-collection.

View File

@ -102,7 +102,7 @@ func TestCounterVecGetMetricWithInvalidLabelValues(t *testing.T) {
Name: "test",
}, []string{"a"})
labelValues := make([]string, len(test.labels))
labelValues := make([]string, 0, len(test.labels))
for _, val := range test.labels {
labelValues = append(labelValues, val)
}

View File

@ -53,9 +53,9 @@ type Desc struct {
// constLabelPairs contains precalculated DTO label pairs based on
// the constant labels.
constLabelPairs []*dto.LabelPair
// variableLabels contains names of labels for which the metric
// maintains variable values.
variableLabels []string
// variableLabels contains names of labels and normalization function for
// which the metric maintains variable values.
variableLabels ConstrainedLabels
// id is a hash of the values of the ConstLabels and fqName. This
// must be unique among all registered descriptors and can therefore be
// used as an identifier of the descriptor.
@ -79,10 +79,24 @@ 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)
}
// 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.
//
// variableLabels only contain the label names and normalization functions. Their
// label values are variable and therefore not part of the Desc. (They are managed
// within the Metric.)
//
// 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 {
d := &Desc{
fqName: fqName,
help: help,
variableLabels: variableLabels,
variableLabels: variableLabels.constrainedLabels(),
}
if !model.IsValidMetricName(model.LabelValue(fqName)) {
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) *
// their sorted label names) plus the fqName (at position 0).
labelValues := make([]string, 1, len(constLabels)+1)
labelValues[0] = fqName
labelNames := make([]string, 0, len(constLabels)+len(variableLabels))
labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels))
labelNameSet := map[string]struct{}{}
// First add only the const label names and sort them...
for labelName := range constLabels {
@ -117,13 +131,13 @@ func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *
// Now add the variable label names, but prefix them with something that
// cannot be in a regular label name. That prevents matching the label
// dimension with a different mix between preset and variable labels.
for _, labelName := range variableLabels {
if !checkLabelName(labelName) {
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName)
for _, label := range d.variableLabels {
if !checkLabelName(label.Name) {
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label.Name, fqName)
return d
}
labelNames = append(labelNames, "$"+labelName)
labelNameSet[labelName] = struct{}{}
labelNames = append(labelNames, "$"+label.Name)
labelNameSet[label.Name] = struct{}{}
}
if len(labelNames) != len(labelNameSet) {
d.err = fmt.Errorf("duplicate label names in constant and variable labels for metric %q", fqName)

View File

@ -294,9 +294,9 @@ func ExampleRegister() {
// Output:
// taskCounter registered.
// 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
// 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
// taskCounter unregistered.
// 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
// 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
// taskCounterVec registered.
// Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"}
// notMyCounter is nil.

View File

@ -55,6 +55,18 @@ type Gauge interface {
// GaugeOpts is an alias for Opts. See there for doc comments.
type GaugeOpts Opts
// GaugeVecOpts bundles the options to create a GaugeVec metric.
// It is mandatory to set GaugeOpts, see there for mandatory fields. VariableLabels
// is optional and can safely be left to its default value.
type GaugeVecOpts struct {
GaugeOpts
// VariableLabels are used to partition the metric vector by the given set
// of labels. Each label value will be constrained with the optional Contraint
// function, if provided.
VariableLabels ConstrainableLabels
}
// NewGauge creates a new Gauge based on the provided GaugeOpts.
//
// The returned implementation is optimized for a fast Set method. If you have a
@ -138,16 +150,24 @@ type GaugeVec struct {
// NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and
// partitioned by the given label names.
func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
desc := NewDesc(
return V2.NewGaugeVec(GaugeVecOpts{
GaugeOpts: opts,
VariableLabels: UnconstrainedLabels(labelNames),
})
}
// NewGaugeVec creates a new GaugeVec based on the provided GaugeVecOpts.
func (v2) NewGaugeVec(opts GaugeVecOpts) *GaugeVec {
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.VariableLabels,
opts.ConstLabels,
)
return &GaugeVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
if len(lvs) != len(desc.variableLabels) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, lvs))
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), lvs))
}
result := &gauge{desc: desc, labelPairs: MakeLabelPairs(desc, lvs)}
result.init(result) // Init self-collection.

View File

@ -469,6 +469,18 @@ type HistogramOpts struct {
NativeHistogramMaxZeroThreshold float64
}
// HistogramVecOpts bundles the options to create a HistogramVec metric.
// It is mandatory to set HistogramOpts, see there for mandatory fields. VariableLabels
// is optional and can safely be left to its default value.
type HistogramVecOpts struct {
HistogramOpts
// VariableLabels are used to partition the metric vector by the given set
// of labels. Each label value will be constrained with the optional Contraint
// function, if provided.
VariableLabels ConstrainableLabels
}
// NewHistogram creates a new Histogram based on the provided HistogramOpts. It
// panics if the buckets in HistogramOpts are not in strictly increasing order.
//
@ -489,11 +501,11 @@ func NewHistogram(opts HistogramOpts) Histogram {
func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogram {
if len(desc.variableLabels) != len(labelValues) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, labelValues))
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), labelValues))
}
for _, n := range desc.variableLabels {
if n == bucketLabel {
if n.Name == bucketLabel {
panic(errBucketLabelNotAllowed)
}
}
@ -1030,15 +1042,23 @@ type HistogramVec struct {
// NewHistogramVec creates a new HistogramVec based on the provided HistogramOpts and
// partitioned by the given label names.
func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
desc := NewDesc(
return V2.NewHistogramVec(HistogramVecOpts{
HistogramOpts: opts,
VariableLabels: UnconstrainedLabels(labelNames),
})
}
// NewHistogramVec creates a new HistogramVec based on the provided HistogramVecOpts.
func (v2) NewHistogramVec(opts HistogramVecOpts) *HistogramVec {
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.VariableLabels,
opts.ConstLabels,
)
return &HistogramVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
return newHistogram(desc, opts, lvs...)
return newHistogram(desc, opts.HistogramOpts, lvs...)
}),
}
}

View File

@ -32,6 +32,78 @@ import (
// create a Desc.
type Labels map[string]string
// ConstrainedLabels represents a label name and its constrain function
// to normalize label values. This type is commonly used when constructing
// metric vector Collectors.
type ConstrainedLabel struct {
Name string
Constraint func(string) string
}
func (cl ConstrainedLabel) Constrain(v string) string {
if cl.Constraint == nil {
return v
}
return cl.Constraint(v)
}
// ConstrainableLabels is an interface that allows creating of labels that can
// be optionally constrained.
//
// prometheus.V2().NewCounterVec(CounterVecOpts{
// CounterOpts: {...}, // Usual CounterOpts fields
// VariableLabels: []ConstrainedLabels{
// {Name: "A"},
// {Name: "B", Constraint: func(v string) string { ... }},
// },
// })
type ConstrainableLabels interface {
constrainedLabels() ConstrainedLabels
labelNames() []string
}
// ConstrainedLabels represents a collection of label name -> constrain function
// to normalize label values. This type is commonly used when constructing
// metric vector Collectors.
type ConstrainedLabels []ConstrainedLabel
func (cls ConstrainedLabels) constrainedLabels() ConstrainedLabels {
return cls
}
func (cls ConstrainedLabels) labelNames() []string {
names := make([]string, len(cls))
for i, label := range cls {
names[i] = label.Name
}
return names
}
// UnconstrainedLabels represents collection of label without any constraint on
// their value. Thus, it is simply a collection of label names.
//
// UnconstrainedLabels([]string{ "A", "B" })
//
// is equivalent to
//
// ConstrainedLabels {
// { Name: "A" },
// { Name: "B" },
// }
type UnconstrainedLabels []string
func (uls UnconstrainedLabels) constrainedLabels() ConstrainedLabels {
constrainedLabels := make([]ConstrainedLabel, len(uls))
for i, l := range uls {
constrainedLabels[i] = ConstrainedLabel{Name: l}
}
return constrainedLabels
}
func (uls UnconstrainedLabels) labelNames() []string {
return uls
}
// reservedLabelPrefix is a prefix which is not legal in user-supplied
// label names.
const reservedLabelPrefix = "__"

View File

@ -962,7 +962,7 @@ func checkDescConsistency(
copy(lpsFromDesc, desc.constLabelPairs)
for _, l := range desc.variableLabels {
lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{
Name: proto.String(l),
Name: proto.String(l.Name),
})
}
if len(lpsFromDesc) != len(dtoMetric.Label) {

View File

@ -148,6 +148,18 @@ type SummaryOpts struct {
BufCap uint32
}
// SummaryVecOpts bundles the options to create a SummaryVec metric.
// It is mandatory to set SummaryOpts, see there for mandatory fields. VariableLabels
// is optional and can safely be left to its default value.
type SummaryVecOpts struct {
SummaryOpts
// VariableLabels are used to partition the metric vector by the given set
// of labels. Each label value will be constrained with the optional Contraint
// function, if provided.
VariableLabels ConstrainableLabels
}
// Problem with the sliding-window decay algorithm... The Merge method of
// perk/quantile is actually not working as advertised - and it might be
// unfixable, as the underlying algorithm is apparently not capable of merging
@ -178,11 +190,11 @@ func NewSummary(opts SummaryOpts) Summary {
func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary {
if len(desc.variableLabels) != len(labelValues) {
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels, labelValues))
panic(makeInconsistentCardinalityError(desc.fqName, desc.variableLabels.labelNames(), labelValues))
}
for _, n := range desc.variableLabels {
if n == quantileLabel {
if n.Name == quantileLabel {
panic(errQuantileLabelNotAllowed)
}
}
@ -530,20 +542,28 @@ type SummaryVec struct {
// it is handled by the Prometheus server internally, “quantile” is an illegal
// label name. NewSummaryVec will panic if this label name is used.
func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
for _, ln := range labelNames {
return V2.NewSummaryVec(SummaryVecOpts{
SummaryOpts: opts,
VariableLabels: UnconstrainedLabels(labelNames),
})
}
// NewSummaryVec creates a new SummaryVec based on the provided SummaryVecOpts.
func (v2) NewSummaryVec(opts SummaryVecOpts) *SummaryVec {
for _, ln := range opts.VariableLabels.labelNames() {
if ln == quantileLabel {
panic(errQuantileLabelNotAllowed)
}
}
desc := NewDesc(
desc := V2.NewDesc(
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labelNames,
opts.VariableLabels,
opts.ConstLabels,
)
return &SummaryVec{
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
return newSummary(desc, opts, lvs...)
return newSummary(desc, opts.SummaryOpts, lvs...)
}),
}
}

View File

@ -188,9 +188,9 @@ func MakeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair {
return desc.constLabelPairs
}
labelPairs := make([]*dto.LabelPair, 0, totalLen)
for i, n := range desc.variableLabels {
for i, l := range desc.variableLabels {
labelPairs = append(labelPairs, &dto.LabelPair{
Name: proto.String(n),
Name: proto.String(l.Name),
Value: proto.String(labelValues[i]),
})
}

View File

@ -72,6 +72,7 @@ func NewMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) *MetricVec {
// with a performance overhead (for creating and processing the Labels map).
// See also the CounterVec example.
func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
lvs = constrainLabelValues(m.desc, lvs, m.curry)
h, err := m.hashLabelValues(lvs)
if err != nil {
return false
@ -91,6 +92,7 @@ func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
// This method is used for the same purpose as DeleteLabelValues(...string). See
// there for pros and cons of the two methods.
func (m *MetricVec) Delete(labels Labels) bool {
labels = constrainLabels(m.desc, labels)
h, err := m.hashLabels(labels)
if err != nil {
return false
@ -106,6 +108,7 @@ func (m *MetricVec) Delete(labels Labels) bool {
// Note that curried labels will never be matched if deleting from the curried vector.
// To match curried labels with DeletePartialMatch, it must be called on the base vector.
func (m *MetricVec) DeletePartialMatch(labels Labels) int {
labels = constrainLabels(m.desc, labels)
return m.metricMap.deleteByLabels(labels, m.curry)
}
@ -145,10 +148,10 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) {
iCurry int
)
for i, label := range m.desc.variableLabels {
val, ok := labels[label]
val, ok := labels[label.Name]
if iCurry < len(oldCurry) && oldCurry[iCurry].index == i {
if ok {
return nil, fmt.Errorf("label name %q is already curried", label)
return nil, fmt.Errorf("label name %q is already curried", label.Name)
}
newCurry = append(newCurry, oldCurry[iCurry])
iCurry++
@ -156,7 +159,7 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) {
if !ok {
continue // Label stays uncurried.
}
newCurry = append(newCurry, curriedLabelValue{i, val})
newCurry = append(newCurry, curriedLabelValue{i, label.Constrain(val)})
}
}
if l := len(oldCurry) + len(labels) - len(newCurry); l > 0 {
@ -199,6 +202,7 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) {
// a wrapper around MetricVec, implementing a vector for a specific Metric
// implementation, for example GaugeVec.
func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
lvs = constrainLabelValues(m.desc, lvs, m.curry)
h, err := m.hashLabelValues(lvs)
if err != nil {
return nil, err
@ -224,6 +228,7 @@ func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
// around MetricVec, implementing a vector for a specific Metric implementation,
// for example GaugeVec.
func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
labels = constrainLabels(m.desc, labels)
h, err := m.hashLabels(labels)
if err != nil {
return nil, err
@ -266,16 +271,16 @@ func (m *MetricVec) hashLabels(labels Labels) (uint64, error) {
iCurry int
)
for i, label := range m.desc.variableLabels {
val, ok := labels[label]
val, ok := labels[label.Name]
if iCurry < len(curry) && curry[iCurry].index == i {
if ok {
return 0, fmt.Errorf("label name %q is already curried", label)
return 0, fmt.Errorf("label name %q is already curried", label.Name)
}
h = m.hashAdd(h, curry[iCurry].value)
iCurry++
} else {
if !ok {
return 0, fmt.Errorf("label name %q missing in label map", label)
return 0, fmt.Errorf("label name %q missing in label map", label.Name)
}
h = m.hashAdd(h, val)
}
@ -453,7 +458,7 @@ func valueMatchesVariableOrCurriedValue(targetValue string, index int, values []
func matchPartialLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool {
for l, v := range labels {
// Check if the target label exists in our metrics and get the index.
varLabelIndex, validLabel := indexOf(l, desc.variableLabels)
varLabelIndex, validLabel := indexOf(l, desc.variableLabels.labelNames())
if validLabel {
// Check the value of that label against the target value.
// We don't consider curried values in partial matches.
@ -605,7 +610,7 @@ func matchLabels(desc *Desc, values []string, labels Labels, curry []curriedLabe
iCurry++
continue
}
if values[i] != labels[k] {
if values[i] != labels[k.Name] {
return false
}
}
@ -621,7 +626,7 @@ func extractLabelValues(desc *Desc, labels Labels, curry []curriedLabelValue) []
iCurry++
continue
}
labelValues[i] = labels[k]
labelValues[i] = labels[k.Name]
}
return labelValues
}
@ -640,3 +645,34 @@ func inlineLabelValues(lvs []string, curry []curriedLabelValue) []string {
}
return labelValues
}
func constrainLabels(desc *Desc, labels Labels) Labels {
constrainedValues := make(Labels, len(labels))
for l, v := range labels {
if i, ok := indexOf(l, desc.variableLabels.labelNames()); ok {
constrainedValues[l] = desc.variableLabels[i].Constrain(v)
continue
}
constrainedValues[l] = v
}
return constrainedValues
}
func constrainLabelValues(desc *Desc, lvs []string, curry []curriedLabelValue) []string {
constrainedValues := make([]string, len(lvs))
var iCurry, iLVs int
for i := 0; i < len(lvs)+len(curry); i++ {
if iCurry < len(curry) && curry[iCurry].index == i {
iCurry++
continue
}
if i < len(desc.variableLabels) {
constrainedValues[iLVs] = desc.variableLabels[i].Constrain(lvs[iLVs])
} else {
constrainedValues[iLVs] = lvs[iLVs]
}
iLVs++
}
return constrainedValues
}

View File

@ -15,6 +15,7 @@ package prometheus
import (
"fmt"
"reflect"
"testing"
dto "github.com/prometheus/client_model/go"
@ -44,6 +45,20 @@ func TestDeleteWithCollisions(t *testing.T) {
testDelete(t, vec)
}
func TestDeleteWithConstraints(t *testing.T) {
vec := V2.NewGaugeVec(GaugeVecOpts{
GaugeOpts{
Name: "test",
Help: "helpless",
},
ConstrainedLabels{
{Name: "l1"},
{Name: "l2", Constraint: func(s string) string { return "x" + s }},
},
})
testDelete(t, vec)
}
func testDelete(t *testing.T, vec *GaugeVec) {
if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), false; got != want {
t.Errorf("got %v, want %v", got, want)
@ -98,6 +113,20 @@ func TestDeleteLabelValuesWithCollisions(t *testing.T) {
testDeleteLabelValues(t, vec)
}
func TestDeleteLabelValuesWithConstraints(t *testing.T) {
vec := V2.NewGaugeVec(GaugeVecOpts{
GaugeOpts{
Name: "test",
Help: "helpless",
},
ConstrainedLabels{
{Name: "l1"},
{Name: "l2", Constraint: func(s string) string { return "x" + s }},
},
})
testDeleteLabelValues(t, vec)
}
func testDeleteLabelValues(t *testing.T, vec *GaugeVec) {
if got, want := vec.DeleteLabelValues("v1", "v2"), false; got != want {
t.Errorf("got %v, want %v", got, want)
@ -126,14 +155,32 @@ func testDeleteLabelValues(t *testing.T, vec *GaugeVec) {
}
func TestDeletePartialMatch(t *testing.T) {
baseVec := NewGaugeVec(
vec := NewGaugeVec(
GaugeOpts{
Name: "test",
Help: "helpless",
},
[]string{"l1", "l2", "l3"},
)
testDeletePartialMatch(t, vec)
}
func TestDeletePartialMatchWithConstraints(t *testing.T) {
vec := V2.NewGaugeVec(GaugeVecOpts{
GaugeOpts{
Name: "test",
Help: "helpless",
},
ConstrainedLabels{
{Name: "l1"},
{Name: "l2", Constraint: func(s string) string { return "x" + s }},
{Name: "l3"},
},
})
testDeletePartialMatch(t, vec)
}
func testDeletePartialMatch(t *testing.T, baseVec *GaugeVec) {
assertNoMetric := func(t *testing.T) {
if n := len(baseVec.metricMap.metrics); n != 0 {
t.Error("expected no metrics, got", n)
@ -293,6 +340,78 @@ func testMetricVec(t *testing.T, vec *GaugeVec) {
}
}
func TestMetricVecWithConstraints(t *testing.T) {
constraint := func(s string) string { return "x" + s }
vec := V2.NewGaugeVec(GaugeVecOpts{
GaugeOpts{
Name: "test",
Help: "helpless",
},
ConstrainedLabels{
{Name: "l1"},
{Name: "l2", Constraint: constraint},
},
})
testConstrainedMetricVec(t, vec, constraint)
}
func testConstrainedMetricVec(t *testing.T, vec *GaugeVec, constrain func(string) string) {
vec.Reset() // Actually test Reset now!
var pair [2]string
// Keep track of metrics.
expected := map[[2]string]int{}
for i := 0; i < 1000; i++ {
pair[0], pair[1] = fmt.Sprint(i%4), fmt.Sprint(i%5) // Varying combinations multiples.
expected[[2]string{pair[0], constrain(pair[1])}]++
vec.WithLabelValues(pair[0], pair[1]).Inc()
expected[[2]string{"v1", constrain("v2")}]++
vec.WithLabelValues("v1", "v2").Inc()
}
var total int
for _, metrics := range vec.metricMap.metrics {
for _, metric := range metrics {
total++
copy(pair[:], metric.values)
var metricOut dto.Metric
if err := metric.metric.Write(&metricOut); err != nil {
t.Fatal(err)
}
actual := *metricOut.Gauge.Value
var actualPair [2]string
for i, label := range metricOut.Label {
actualPair[i] = *label.Value
}
// Test output pair against metric.values to ensure we've selected
// the right one. We check this to ensure the below check means
// anything at all.
if actualPair != pair {
t.Fatalf("unexpected pair association in metric map: %v != %v", actualPair, pair)
}
if actual != float64(expected[pair]) {
t.Fatalf("incorrect counter value for %v: %v != %v", pair, actual, expected[pair])
}
}
}
if total != len(expected) {
t.Fatalf("unexpected number of metrics: %v != %v", total, len(expected))
}
vec.Reset()
if len(vec.metricMap.metrics) > 0 {
t.Fatalf("reset failed")
}
}
func TestCounterVecEndToEndWithCollision(t *testing.T) {
vec := NewCounterVec(
CounterOpts{
@ -350,6 +469,39 @@ func TestCurryVecWithCollisions(t *testing.T) {
testCurryVec(t, vec)
}
func TestCurryVecWithConstraints(t *testing.T) {
constraint := func(s string) string { return "x" + s }
t.Run("constrainedLabels overlap variableLabels", func(t *testing.T) {
vec := V2.NewCounterVec(CounterVecOpts{
CounterOpts{
Name: "test",
Help: "helpless",
},
ConstrainedLabels{
{Name: "one"},
{Name: "two"},
{Name: "three", Constraint: constraint},
},
})
testCurryVec(t, vec)
})
t.Run("constrainedLabels reducing cardinality", func(t *testing.T) {
constraint := func(s string) string { return "x" }
vec := V2.NewCounterVec(CounterVecOpts{
CounterOpts{
Name: "test",
Help: "helpless",
},
ConstrainedLabels{
{Name: "one"},
{Name: "two"},
{Name: "three", Constraint: constraint},
},
})
testConstrainedCurryVec(t, vec, constraint)
})
}
func testCurryVec(t *testing.T, vec *CounterVec) {
assertMetrics := func(t *testing.T) {
n := 0
@ -547,6 +699,211 @@ func testCurryVec(t *testing.T, vec *CounterVec) {
})
}
func testConstrainedCurryVec(t *testing.T, vec *CounterVec, constraint func(string) string) {
assertMetrics := func(t *testing.T) {
n := 0
for _, m := range vec.metricMap.metrics {
n += len(m)
}
if n != 2 {
t.Error("expected two metrics, got", n)
}
m := &dto.Metric{}
c1, err := vec.GetMetricWithLabelValues("1", "2", "3")
if err != nil {
t.Fatal("unexpected error getting metric:", err)
}
c1.Write(m)
if want, got := 1., m.GetCounter().GetValue(); want != got {
t.Errorf("want %f as counter value, got %f", want, got)
}
values := map[string]string{}
for _, label := range m.Label {
values[*label.Name] = *label.Value
}
if want, got := map[string]string{"one": "1", "two": "2", "three": constraint("3")}, values; !reflect.DeepEqual(want, got) {
t.Errorf("want %v as label values, got %v", want, got)
}
m.Reset()
c2, err := vec.GetMetricWithLabelValues("11", "22", "33")
if err != nil {
t.Fatal("unexpected error getting metric:", err)
}
c2.Write(m)
if want, got := 1., m.GetCounter().GetValue(); want != got {
t.Errorf("want %f as counter value, got %f", want, got)
}
values = map[string]string{}
for _, label := range m.Label {
values[*label.Name] = *label.Value
}
if want, got := map[string]string{"one": "11", "two": "22", "three": constraint("33")}, values; !reflect.DeepEqual(want, got) {
t.Errorf("want %v as label values, got %v", want, got)
}
}
assertNoMetric := func(t *testing.T) {
if n := len(vec.metricMap.metrics); n != 0 {
t.Error("expected no metrics, got", n)
}
}
t.Run("zero labels", func(t *testing.T) {
c1 := vec.MustCurryWith(nil)
c2 := vec.MustCurryWith(nil)
c1.WithLabelValues("1", "2", "3").Inc()
c2.With(Labels{"one": "11", "two": "22", "three": "33"}).Inc()
assertMetrics(t)
if !c1.Delete(Labels{"one": "1", "two": "2", "three": "3"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("11", "22", "33") {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("first label", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"one": "1"})
c2 := vec.MustCurryWith(Labels{"one": "11"})
c1.WithLabelValues("2", "3").Inc()
c2.With(Labels{"two": "22", "three": "33"}).Inc()
assertMetrics(t)
if c1.Delete(Labels{"two": "22", "three": "33"}) {
t.Error("deletion unexpectedly succeeded")
}
if c2.DeleteLabelValues("2", "3") {
t.Error("deletion unexpectedly succeeded")
}
if !c1.Delete(Labels{"two": "2", "three": "3"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("22", "33") {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("middle label", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"two": "2"})
c2 := vec.MustCurryWith(Labels{"two": "22"})
c1.WithLabelValues("1", "3").Inc()
c2.With(Labels{"one": "11", "three": "33"}).Inc()
assertMetrics(t)
if c1.Delete(Labels{"one": "11", "three": "33"}) {
t.Error("deletion unexpectedly succeeded")
}
if c2.DeleteLabelValues("1", "3") {
t.Error("deletion unexpectedly succeeded")
}
if !c1.Delete(Labels{"one": "1", "three": "3"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("11", "33") {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("last label (constrained to static value)", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"three": "3"})
c2 := vec.MustCurryWith(Labels{"three": "33"})
c1.WithLabelValues("1", "2").Inc()
c2.With(Labels{"one": "11", "two": "22"}).Inc()
assertMetrics(t)
if !c1.Delete(Labels{"two": "22", "one": "11"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("1", "2") {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("two labels", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"three": "3", "one": "1"})
c2 := vec.MustCurryWith(Labels{"three": "33", "one": "11"})
c1.WithLabelValues("2").Inc()
c2.With(Labels{"two": "22"}).Inc()
assertMetrics(t)
if c1.Delete(Labels{"two": "22"}) {
t.Error("deletion unexpectedly succeeded")
}
if c2.DeleteLabelValues("2") {
t.Error("deletion unexpectedly succeeded")
}
if !c1.Delete(Labels{"two": "2"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("22") {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("all labels", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"three": "3", "two": "2", "one": "1"})
c2 := vec.MustCurryWith(Labels{"three": "33", "one": "11", "two": "22"})
c1.WithLabelValues().Inc()
c2.With(nil).Inc()
assertMetrics(t)
if !c1.Delete(Labels{}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues() {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("double curry", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"three": "3"}).MustCurryWith(Labels{"one": "1"})
c2 := vec.MustCurryWith(Labels{"three": "33"}).MustCurryWith(Labels{"one": "11"})
c1.WithLabelValues("2").Inc()
c2.With(Labels{"two": "22"}).Inc()
assertMetrics(t)
if c1.Delete(Labels{"two": "22"}) {
t.Error("deletion unexpectedly succeeded")
}
if c2.DeleteLabelValues("2") {
t.Error("deletion unexpectedly succeeded")
}
if !c1.Delete(Labels{"two": "2"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("22") {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("use already curried label", func(t *testing.T) {
c1 := vec.MustCurryWith(Labels{"three": "3"})
if _, err := c1.GetMetricWithLabelValues("1", "2", "3"); err == nil {
t.Error("expected error when using already curried label")
}
if _, err := c1.GetMetricWith(Labels{"one": "1", "two": "2", "three": "3"}); err == nil {
t.Error("expected error when using already curried label")
}
assertNoMetric(t)
c1.WithLabelValues("1", "2").Inc()
if c1.Delete(Labels{"one": "1", "two": "2", "three": "3"}) {
t.Error("deletion unexpectedly succeeded")
}
if !c1.Delete(Labels{"one": "1", "two": "2"}) {
t.Error("deletion failed")
}
assertNoMetric(t)
})
t.Run("curry already curried label", func(t *testing.T) {
if _, err := vec.MustCurryWith(Labels{"three": "3"}).CurryWith(Labels{"three": "33"}); err == nil {
t.Error("currying unexpectedly succeeded")
} else if err.Error() != `label name "three" is already curried` {
t.Error("currying returned unexpected error:", err)
}
})
t.Run("unknown label", func(t *testing.T) {
if _, err := vec.CurryWith(Labels{"foo": "bar"}); err == nil {
t.Error("currying unexpectedly succeeded")
} else if err.Error() != "1 unknown label(s) found during currying" {
t.Error("currying returned unexpected error:", err)
}
})
}
func BenchmarkMetricVecWithLabelValuesBasic(b *testing.B) {
benchmarkMetricVecWithLabelValues(b, map[string][]string{
"l1": {"onevalue"},

23
prometheus/vnext.go Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2022 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus
type v2 struct{}
// V2 is a struct that can be referenced to access experimental API that might
// be present in v2 of client golang someday. It offers extended functionality
// of v1 with slightly changed API. It is acceptable to use some pieces from v1
// and e.g `prometheus.NewGauge` and some from v2 e.g. `prometheus.V2.NewDesc`
// in the same codebase.
var V2 = v2{}

View File

@ -206,7 +206,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc {
constLabels[ln] = lv
}
// NewDesc will do remaining validations.
newDesc := NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels)
newDesc := V2.NewDesc(prefix+desc.fqName, desc.help, desc.variableLabels, constLabels)
// Propagate errors if there was any. This will override any errer
// created by NewDesc above, i.e. earlier errors get precedence.
if desc.err != nil {