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:
parent
3d765a161a
commit
fae2f6306b
|
@ -59,6 +59,18 @@ type ExemplarAdder interface {
|
||||||
// CounterOpts is an alias for Opts. See there for doc comments.
|
// CounterOpts is an alias for Opts. See there for doc comments.
|
||||||
type CounterOpts Opts
|
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.
|
// NewCounter creates a new Counter based on the provided CounterOpts.
|
||||||
//
|
//
|
||||||
// The returned implementation also implements ExemplarAdder. It is safe to
|
// 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
|
// NewCounterVec creates a new CounterVec based on the provided CounterOpts and
|
||||||
// partitioned by the given label names.
|
// partitioned by the given label names.
|
||||||
func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
|
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),
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
opts.Help,
|
opts.Help,
|
||||||
labelNames,
|
opts.VariableLabels,
|
||||||
opts.ConstLabels,
|
opts.ConstLabels,
|
||||||
)
|
)
|
||||||
return &CounterVec{
|
return &CounterVec{
|
||||||
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
||||||
if len(lvs) != len(desc.variableLabels) {
|
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 := &counter{desc: desc, labelPairs: MakeLabelPairs(desc, lvs), now: time.Now}
|
||||||
result.init(result) // Init self-collection.
|
result.init(result) // Init self-collection.
|
||||||
|
|
|
@ -102,7 +102,7 @@ func TestCounterVecGetMetricWithInvalidLabelValues(t *testing.T) {
|
||||||
Name: "test",
|
Name: "test",
|
||||||
}, []string{"a"})
|
}, []string{"a"})
|
||||||
|
|
||||||
labelValues := make([]string, len(test.labels))
|
labelValues := make([]string, 0, len(test.labels))
|
||||||
for _, val := range test.labels {
|
for _, val := range test.labels {
|
||||||
labelValues = append(labelValues, val)
|
labelValues = append(labelValues, val)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,9 +53,9 @@ type Desc struct {
|
||||||
// constLabelPairs contains precalculated DTO label pairs based on
|
// constLabelPairs contains precalculated DTO label pairs based on
|
||||||
// the constant labels.
|
// the constant labels.
|
||||||
constLabelPairs []*dto.LabelPair
|
constLabelPairs []*dto.LabelPair
|
||||||
// variableLabels contains names of labels for which the metric
|
// variableLabels contains names of labels and normalization function for
|
||||||
// maintains variable values.
|
// which the metric maintains variable values.
|
||||||
variableLabels []string
|
variableLabels ConstrainedLabels
|
||||||
// id is a hash of the values of the ConstLabels and fqName. This
|
// id is a hash of the values of the ConstLabels and fqName. This
|
||||||
// must be unique among all registered descriptors and can therefore be
|
// must be unique among all registered descriptors and can therefore be
|
||||||
// used as an identifier of the descriptor.
|
// 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
|
// For constLabels, the label values are constant. Therefore, they are fully
|
||||||
// specified in the Desc. See the Collector example for a usage pattern.
|
// specified in the Desc. See the Collector example for a usage pattern.
|
||||||
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
|
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{
|
d := &Desc{
|
||||||
fqName: fqName,
|
fqName: fqName,
|
||||||
help: help,
|
help: help,
|
||||||
variableLabels: variableLabels,
|
variableLabels: variableLabels.constrainedLabels(),
|
||||||
}
|
}
|
||||||
if !model.IsValidMetricName(model.LabelValue(fqName)) {
|
if !model.IsValidMetricName(model.LabelValue(fqName)) {
|
||||||
d.err = fmt.Errorf("%q is not a valid metric name", 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).
|
// their sorted label names) plus the fqName (at position 0).
|
||||||
labelValues := make([]string, 1, len(constLabels)+1)
|
labelValues := make([]string, 1, len(constLabels)+1)
|
||||||
labelValues[0] = fqName
|
labelValues[0] = fqName
|
||||||
labelNames := make([]string, 0, len(constLabels)+len(variableLabels))
|
labelNames := make([]string, 0, len(constLabels)+len(d.variableLabels))
|
||||||
labelNameSet := map[string]struct{}{}
|
labelNameSet := map[string]struct{}{}
|
||||||
// First add only the const label names and sort them...
|
// First add only the const label names and sort them...
|
||||||
for labelName := range constLabels {
|
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
|
// Now add the variable label names, but prefix them with something that
|
||||||
// cannot be in a regular label name. That prevents matching the label
|
// cannot be in a regular label name. That prevents matching the label
|
||||||
// dimension with a different mix between preset and variable labels.
|
// dimension with a different mix between preset and variable labels.
|
||||||
for _, labelName := range variableLabels {
|
for _, label := range d.variableLabels {
|
||||||
if !checkLabelName(labelName) {
|
if !checkLabelName(label.Name) {
|
||||||
d.err = fmt.Errorf("%q is not a valid label name for metric %q", labelName, fqName)
|
d.err = fmt.Errorf("%q is not a valid label name for metric %q", label.Name, fqName)
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
labelNames = append(labelNames, "$"+labelName)
|
labelNames = append(labelNames, "$"+label.Name)
|
||||||
labelNameSet[labelName] = struct{}{}
|
labelNameSet[label.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
if len(labelNames) != len(labelNameSet) {
|
if len(labelNames) != len(labelNameSet) {
|
||||||
d.err = fmt.Errorf("duplicate label names in constant and variable labels for metric %q", fqName)
|
d.err = fmt.Errorf("duplicate label names in constant and variable labels for metric %q", fqName)
|
||||||
|
|
|
@ -294,9 +294,9 @@ func ExampleRegister() {
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// taskCounter registered.
|
// 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.
|
// 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.
|
// taskCounterVec registered.
|
||||||
// Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"}
|
// Worker initialization failed: inconsistent label cardinality: expected 1 label values but got 2 in []string{"42", "spurious arg"}
|
||||||
// notMyCounter is nil.
|
// notMyCounter is nil.
|
||||||
|
|
|
@ -55,6 +55,18 @@ type Gauge interface {
|
||||||
// GaugeOpts is an alias for Opts. See there for doc comments.
|
// GaugeOpts is an alias for Opts. See there for doc comments.
|
||||||
type GaugeOpts Opts
|
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.
|
// NewGauge creates a new Gauge based on the provided GaugeOpts.
|
||||||
//
|
//
|
||||||
// The returned implementation is optimized for a fast Set method. If you have a
|
// 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
|
// NewGaugeVec creates a new GaugeVec based on the provided GaugeOpts and
|
||||||
// partitioned by the given label names.
|
// partitioned by the given label names.
|
||||||
func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
|
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),
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
opts.Help,
|
opts.Help,
|
||||||
labelNames,
|
opts.VariableLabels,
|
||||||
opts.ConstLabels,
|
opts.ConstLabels,
|
||||||
)
|
)
|
||||||
return &GaugeVec{
|
return &GaugeVec{
|
||||||
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
||||||
if len(lvs) != len(desc.variableLabels) {
|
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 := &gauge{desc: desc, labelPairs: MakeLabelPairs(desc, lvs)}
|
||||||
result.init(result) // Init self-collection.
|
result.init(result) // Init self-collection.
|
||||||
|
|
|
@ -469,6 +469,18 @@ type HistogramOpts struct {
|
||||||
NativeHistogramMaxZeroThreshold float64
|
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
|
// NewHistogram creates a new Histogram based on the provided HistogramOpts. It
|
||||||
// panics if the buckets in HistogramOpts are not in strictly increasing order.
|
// 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 {
|
func newHistogram(desc *Desc, opts HistogramOpts, labelValues ...string) Histogram {
|
||||||
if len(desc.variableLabels) != len(labelValues) {
|
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 {
|
for _, n := range desc.variableLabels {
|
||||||
if n == bucketLabel {
|
if n.Name == bucketLabel {
|
||||||
panic(errBucketLabelNotAllowed)
|
panic(errBucketLabelNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1030,15 +1042,23 @@ type HistogramVec struct {
|
||||||
// NewHistogramVec creates a new HistogramVec based on the provided HistogramOpts and
|
// NewHistogramVec creates a new HistogramVec based on the provided HistogramOpts and
|
||||||
// partitioned by the given label names.
|
// partitioned by the given label names.
|
||||||
func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
|
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),
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
opts.Help,
|
opts.Help,
|
||||||
labelNames,
|
opts.VariableLabels,
|
||||||
opts.ConstLabels,
|
opts.ConstLabels,
|
||||||
)
|
)
|
||||||
return &HistogramVec{
|
return &HistogramVec{
|
||||||
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
||||||
return newHistogram(desc, opts, lvs...)
|
return newHistogram(desc, opts.HistogramOpts, lvs...)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,78 @@ import (
|
||||||
// create a Desc.
|
// create a Desc.
|
||||||
type Labels map[string]string
|
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
|
// reservedLabelPrefix is a prefix which is not legal in user-supplied
|
||||||
// label names.
|
// label names.
|
||||||
const reservedLabelPrefix = "__"
|
const reservedLabelPrefix = "__"
|
||||||
|
|
|
@ -962,7 +962,7 @@ func checkDescConsistency(
|
||||||
copy(lpsFromDesc, desc.constLabelPairs)
|
copy(lpsFromDesc, desc.constLabelPairs)
|
||||||
for _, l := range desc.variableLabels {
|
for _, l := range desc.variableLabels {
|
||||||
lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{
|
lpsFromDesc = append(lpsFromDesc, &dto.LabelPair{
|
||||||
Name: proto.String(l),
|
Name: proto.String(l.Name),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(lpsFromDesc) != len(dtoMetric.Label) {
|
if len(lpsFromDesc) != len(dtoMetric.Label) {
|
||||||
|
|
|
@ -148,6 +148,18 @@ type SummaryOpts struct {
|
||||||
BufCap uint32
|
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
|
// Problem with the sliding-window decay algorithm... The Merge method of
|
||||||
// perk/quantile is actually not working as advertised - and it might be
|
// perk/quantile is actually not working as advertised - and it might be
|
||||||
// unfixable, as the underlying algorithm is apparently not capable of merging
|
// 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 {
|
func newSummary(desc *Desc, opts SummaryOpts, labelValues ...string) Summary {
|
||||||
if len(desc.variableLabels) != len(labelValues) {
|
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 {
|
for _, n := range desc.variableLabels {
|
||||||
if n == quantileLabel {
|
if n.Name == quantileLabel {
|
||||||
panic(errQuantileLabelNotAllowed)
|
panic(errQuantileLabelNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -530,20 +542,28 @@ type SummaryVec struct {
|
||||||
// it is handled by the Prometheus server internally, “quantile” is an illegal
|
// it is handled by the Prometheus server internally, “quantile” is an illegal
|
||||||
// label name. NewSummaryVec will panic if this label name is used.
|
// label name. NewSummaryVec will panic if this label name is used.
|
||||||
func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
|
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 {
|
if ln == quantileLabel {
|
||||||
panic(errQuantileLabelNotAllowed)
|
panic(errQuantileLabelNotAllowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
desc := NewDesc(
|
desc := V2.NewDesc(
|
||||||
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
opts.Help,
|
opts.Help,
|
||||||
labelNames,
|
opts.VariableLabels,
|
||||||
opts.ConstLabels,
|
opts.ConstLabels,
|
||||||
)
|
)
|
||||||
return &SummaryVec{
|
return &SummaryVec{
|
||||||
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
MetricVec: NewMetricVec(desc, func(lvs ...string) Metric {
|
||||||
return newSummary(desc, opts, lvs...)
|
return newSummary(desc, opts.SummaryOpts, lvs...)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,9 +188,9 @@ func MakeLabelPairs(desc *Desc, labelValues []string) []*dto.LabelPair {
|
||||||
return desc.constLabelPairs
|
return desc.constLabelPairs
|
||||||
}
|
}
|
||||||
labelPairs := make([]*dto.LabelPair, 0, totalLen)
|
labelPairs := make([]*dto.LabelPair, 0, totalLen)
|
||||||
for i, n := range desc.variableLabels {
|
for i, l := range desc.variableLabels {
|
||||||
labelPairs = append(labelPairs, &dto.LabelPair{
|
labelPairs = append(labelPairs, &dto.LabelPair{
|
||||||
Name: proto.String(n),
|
Name: proto.String(l.Name),
|
||||||
Value: proto.String(labelValues[i]),
|
Value: proto.String(labelValues[i]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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).
|
// with a performance overhead (for creating and processing the Labels map).
|
||||||
// See also the CounterVec example.
|
// See also the CounterVec example.
|
||||||
func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
|
func (m *MetricVec) DeleteLabelValues(lvs ...string) bool {
|
||||||
|
lvs = constrainLabelValues(m.desc, lvs, m.curry)
|
||||||
h, err := m.hashLabelValues(lvs)
|
h, err := m.hashLabelValues(lvs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
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
|
// This method is used for the same purpose as DeleteLabelValues(...string). See
|
||||||
// there for pros and cons of the two methods.
|
// there for pros and cons of the two methods.
|
||||||
func (m *MetricVec) Delete(labels Labels) bool {
|
func (m *MetricVec) Delete(labels Labels) bool {
|
||||||
|
labels = constrainLabels(m.desc, labels)
|
||||||
h, err := m.hashLabels(labels)
|
h, err := m.hashLabels(labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
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.
|
// 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.
|
// To match curried labels with DeletePartialMatch, it must be called on the base vector.
|
||||||
func (m *MetricVec) DeletePartialMatch(labels Labels) int {
|
func (m *MetricVec) DeletePartialMatch(labels Labels) int {
|
||||||
|
labels = constrainLabels(m.desc, labels)
|
||||||
return m.metricMap.deleteByLabels(labels, m.curry)
|
return m.metricMap.deleteByLabels(labels, m.curry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,10 +148,10 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) {
|
||||||
iCurry int
|
iCurry int
|
||||||
)
|
)
|
||||||
for i, label := range m.desc.variableLabels {
|
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 iCurry < len(oldCurry) && oldCurry[iCurry].index == i {
|
||||||
if ok {
|
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])
|
newCurry = append(newCurry, oldCurry[iCurry])
|
||||||
iCurry++
|
iCurry++
|
||||||
|
@ -156,7 +159,7 @@ func (m *MetricVec) CurryWith(labels Labels) (*MetricVec, error) {
|
||||||
if !ok {
|
if !ok {
|
||||||
continue // Label stays uncurried.
|
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 {
|
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
|
// a wrapper around MetricVec, implementing a vector for a specific Metric
|
||||||
// implementation, for example GaugeVec.
|
// implementation, for example GaugeVec.
|
||||||
func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
|
func (m *MetricVec) GetMetricWithLabelValues(lvs ...string) (Metric, error) {
|
||||||
|
lvs = constrainLabelValues(m.desc, lvs, m.curry)
|
||||||
h, err := m.hashLabelValues(lvs)
|
h, err := m.hashLabelValues(lvs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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,
|
// around MetricVec, implementing a vector for a specific Metric implementation,
|
||||||
// for example GaugeVec.
|
// for example GaugeVec.
|
||||||
func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
|
func (m *MetricVec) GetMetricWith(labels Labels) (Metric, error) {
|
||||||
|
labels = constrainLabels(m.desc, labels)
|
||||||
h, err := m.hashLabels(labels)
|
h, err := m.hashLabels(labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -266,16 +271,16 @@ func (m *MetricVec) hashLabels(labels Labels) (uint64, error) {
|
||||||
iCurry int
|
iCurry int
|
||||||
)
|
)
|
||||||
for i, label := range m.desc.variableLabels {
|
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 iCurry < len(curry) && curry[iCurry].index == i {
|
||||||
if ok {
|
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)
|
h = m.hashAdd(h, curry[iCurry].value)
|
||||||
iCurry++
|
iCurry++
|
||||||
} else {
|
} else {
|
||||||
if !ok {
|
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)
|
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 {
|
func matchPartialLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool {
|
||||||
for l, v := range labels {
|
for l, v := range labels {
|
||||||
// Check if the target label exists in our metrics and get the index.
|
// 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 {
|
if validLabel {
|
||||||
// Check the value of that label against the target value.
|
// Check the value of that label against the target value.
|
||||||
// We don't consider curried values in partial matches.
|
// We don't consider curried values in partial matches.
|
||||||
|
@ -605,7 +610,7 @@ func matchLabels(desc *Desc, values []string, labels Labels, curry []curriedLabe
|
||||||
iCurry++
|
iCurry++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if values[i] != labels[k] {
|
if values[i] != labels[k.Name] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -621,7 +626,7 @@ func extractLabelValues(desc *Desc, labels Labels, curry []curriedLabelValue) []
|
||||||
iCurry++
|
iCurry++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
labelValues[i] = labels[k]
|
labelValues[i] = labels[k.Name]
|
||||||
}
|
}
|
||||||
return labelValues
|
return labelValues
|
||||||
}
|
}
|
||||||
|
@ -640,3 +645,34 @@ func inlineLabelValues(lvs []string, curry []curriedLabelValue) []string {
|
||||||
}
|
}
|
||||||
return labelValues
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ package prometheus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
@ -44,6 +45,20 @@ func TestDeleteWithCollisions(t *testing.T) {
|
||||||
testDelete(t, vec)
|
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) {
|
func testDelete(t *testing.T, vec *GaugeVec) {
|
||||||
if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), false; got != want {
|
if got, want := vec.Delete(Labels{"l1": "v1", "l2": "v2"}), false; got != want {
|
||||||
t.Errorf("got %v, want %v", got, want)
|
t.Errorf("got %v, want %v", got, want)
|
||||||
|
@ -98,6 +113,20 @@ func TestDeleteLabelValuesWithCollisions(t *testing.T) {
|
||||||
testDeleteLabelValues(t, vec)
|
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) {
|
func testDeleteLabelValues(t *testing.T, vec *GaugeVec) {
|
||||||
if got, want := vec.DeleteLabelValues("v1", "v2"), false; got != want {
|
if got, want := vec.DeleteLabelValues("v1", "v2"), false; got != want {
|
||||||
t.Errorf("got %v, want %v", 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) {
|
func TestDeletePartialMatch(t *testing.T) {
|
||||||
baseVec := NewGaugeVec(
|
vec := NewGaugeVec(
|
||||||
GaugeOpts{
|
GaugeOpts{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Help: "helpless",
|
Help: "helpless",
|
||||||
},
|
},
|
||||||
[]string{"l1", "l2", "l3"},
|
[]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) {
|
assertNoMetric := func(t *testing.T) {
|
||||||
if n := len(baseVec.metricMap.metrics); n != 0 {
|
if n := len(baseVec.metricMap.metrics); n != 0 {
|
||||||
t.Error("expected no metrics, got", n)
|
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) {
|
func TestCounterVecEndToEndWithCollision(t *testing.T) {
|
||||||
vec := NewCounterVec(
|
vec := NewCounterVec(
|
||||||
CounterOpts{
|
CounterOpts{
|
||||||
|
@ -350,6 +469,39 @@ func TestCurryVecWithCollisions(t *testing.T) {
|
||||||
testCurryVec(t, vec)
|
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) {
|
func testCurryVec(t *testing.T, vec *CounterVec) {
|
||||||
assertMetrics := func(t *testing.T) {
|
assertMetrics := func(t *testing.T) {
|
||||||
n := 0
|
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) {
|
func BenchmarkMetricVecWithLabelValuesBasic(b *testing.B) {
|
||||||
benchmarkMetricVecWithLabelValues(b, map[string][]string{
|
benchmarkMetricVecWithLabelValues(b, map[string][]string{
|
||||||
"l1": {"onevalue"},
|
"l1": {"onevalue"},
|
||||||
|
|
|
@ -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{}
|
|
@ -206,7 +206,7 @@ func wrapDesc(desc *Desc, prefix string, labels Labels) *Desc {
|
||||||
constLabels[ln] = lv
|
constLabels[ln] = lv
|
||||||
}
|
}
|
||||||
// NewDesc will do remaining validations.
|
// 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
|
// Propagate errors if there was any. This will override any errer
|
||||||
// created by NewDesc above, i.e. earlier errors get precedence.
|
// created by NewDesc above, i.e. earlier errors get precedence.
|
||||||
if desc.err != nil {
|
if desc.err != nil {
|
||||||
|
|
Loading…
Reference in New Issue