Allow currying of metric vec's

The idea behind it is described in detail in
https://github.com/prometheus/client_golang/issues/320 .

This commit also updates the example given in
promhttp/instrument_server_test.go , which nicely illustrates the
benefit of this change.

So far, currying could be emulated by creating different metric vec's
with different values in their ConstLabels. This was quite difficult
to grasp - which is essentially what was done in the example mentioned
above. Now that this use case can be solved without ConstLabels, we
can safely declare ConstLabels as rarely used. (Perhaps we can
deprecate them entirely one day, but I'll take a raincheck on that
when the changes of v0.10 have materialized.) This commit thus also
updates the ConstLabel doc comments in the various Opts. (It contained
fairly outdated stuff anyway.)
This commit is contained in:
beorn7 2017-08-30 01:05:29 +02:00
parent 10c55533cb
commit dd20712622
10 changed files with 683 additions and 251 deletions

View File

@ -111,7 +111,7 @@ func NewCounterVec(opts CounterOpts, labelNames []string) *CounterVec {
// Counter with the same label values is created later. // Counter with the same label values is created later.
// //
// An error is returned if the number of label values is not the same as the // An error is returned if the number of label values is not the same as the
// number of VariableLabels in Desc. // number of VariableLabels in Desc (minus any curried labels).
// //
// Note that for more than one label value, this method is prone to mistakes // Note that for more than one label value, this method is prone to mistakes
// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as // caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as
@ -134,7 +134,7 @@ func (v *CounterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) {
// the same as for GetMetricWithLabelValues. // the same as for GetMetricWithLabelValues.
// //
// An error is returned if the number and names of the Labels are inconsistent // An error is returned if the number and names of the Labels are inconsistent
// with those of the VariableLabels in Desc. // with those of the VariableLabels in Desc (minus any curried labels).
// //
// This method is used for the same purpose as // This method is used for the same purpose as
// GetMetricWithLabelValues(...string). See there for pros and cons of the two // GetMetricWithLabelValues(...string). See there for pros and cons of the two
@ -148,8 +148,8 @@ func (v *CounterVec) GetMetricWith(labels Labels) (Counter, error) {
} }
// WithLabelValues works as GetMetricWithLabelValues, but panics where // WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an // GetMetricWithLabelValues would have returned an error. Not returning an
// error, WithLabelValues allows shortcuts like // error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42) // myVec.WithLabelValues("404", "GET").Add(42)
func (v *CounterVec) WithLabelValues(lvs ...string) Counter { func (v *CounterVec) WithLabelValues(lvs ...string) Counter {
c, err := v.GetMetricWithLabelValues(lvs...) c, err := v.GetMetricWithLabelValues(lvs...)
@ -160,8 +160,8 @@ func (v *CounterVec) WithLabelValues(lvs ...string) Counter {
} }
// With works as GetMetricWith, but panics where GetMetricWithLabels would have // With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like // returned an error. Not returning an error allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) // myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Add(42)
func (v *CounterVec) With(labels Labels) Counter { func (v *CounterVec) With(labels Labels) Counter {
c, err := v.GetMetricWith(labels) c, err := v.GetMetricWith(labels)
if err != nil { if err != nil {
@ -170,6 +170,37 @@ func (v *CounterVec) With(labels Labels) Counter {
return c return c
} }
// CurryWith returns a vector curried with the provided labels, i.e. the
// returned vector has those labels pre-set for all labeled operations performed
// on it. The cardinality of the curried vector is reduced accordingly. The
// order of the remaining labels stays the same (just with the curried labels
// taken out of the sequence which is relevant for the
// (GetMetric)WithLabelValues methods). It is possible to curry a curried
// vector, but only with labels not yet used for currying before.
//
// The metrics contained in the CounterVec are shared between the curried and
// uncurried vectors. They are just accessed differently. Curried and uncurried
// vectors behave identically in terms of collection. Only one must be
// registered with a given registry (usually the uncurried version). The Reset
// method deletes all metrics, even if called on a curried vector.
func (v *CounterVec) CurryWith(labels Labels) (*CounterVec, error) {
vec, err := v.curryWith(labels)
if vec != nil {
return &CounterVec{vec}, err
}
return nil, err
}
// MustCurryWith works as CurryWith but panics where CurryWith would have
// returned an error.
func (v *CounterVec) MustCurryWith(labels Labels) *CounterVec {
vec, err := v.CurryWith(labels)
if err != nil {
panic(err)
}
return vec
}
// CounterFunc is a Counter whose value is determined at collect time by calling a // CounterFunc is a Counter whose value is determined at collect time by calling a
// provided function. // provided function.
// //

View File

@ -73,8 +73,7 @@ type Desc struct {
// and therefore not part of the Desc. (They are managed within the Metric.) // and therefore not part of the Desc. (They are managed within the Metric.)
// //
// 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 Opts documentation for the implications of // specified in the Desc. See the Collector example for a usage pattern.
// constant labels.
func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc { func NewDesc(fqName, help string, variableLabels []string, constLabels Labels) *Desc {
d := &Desc{ d := &Desc{
fqName: fqName, fqName: fqName,

View File

@ -98,7 +98,7 @@ func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
// example. // example.
// //
// An error is returned if the number of label values is not the same as the // An error is returned if the number of label values is not the same as the
// number of VariableLabels in Desc. // number of VariableLabels in Desc (minus any curried labels).
// //
// Note that for more than one label value, this method is prone to mistakes // Note that for more than one label value, this method is prone to mistakes
// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as // caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as
@ -120,7 +120,7 @@ func (v *GaugeVec) GetMetricWithLabelValues(lvs ...string) (Gauge, error) {
// the same as for GetMetricWithLabelValues. // the same as for GetMetricWithLabelValues.
// //
// An error is returned if the number and names of the Labels are inconsistent // An error is returned if the number and names of the Labels are inconsistent
// with those of the VariableLabels in Desc. // with those of the VariableLabels in Desc (minus any curried labels).
// //
// This method is used for the same purpose as // This method is used for the same purpose as
// GetMetricWithLabelValues(...string). See there for pros and cons of the two // GetMetricWithLabelValues(...string). See there for pros and cons of the two
@ -134,8 +134,8 @@ func (v *GaugeVec) GetMetricWith(labels Labels) (Gauge, error) {
} }
// WithLabelValues works as GetMetricWithLabelValues, but panics where // WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an // GetMetricWithLabelValues would have returned an error. Not returning an
// error, WithLabelValues allows shortcuts like // error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42) // myVec.WithLabelValues("404", "GET").Add(42)
func (v *GaugeVec) WithLabelValues(lvs ...string) Gauge { func (v *GaugeVec) WithLabelValues(lvs ...string) Gauge {
g, err := v.GetMetricWithLabelValues(lvs...) g, err := v.GetMetricWithLabelValues(lvs...)
@ -146,8 +146,8 @@ func (v *GaugeVec) WithLabelValues(lvs ...string) Gauge {
} }
// With works as GetMetricWith, but panics where GetMetricWithLabels would have // With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like // returned an error. Not returning an error allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42) // myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Add(42)
func (v *GaugeVec) With(labels Labels) Gauge { func (v *GaugeVec) With(labels Labels) Gauge {
g, err := v.GetMetricWith(labels) g, err := v.GetMetricWith(labels)
if err != nil { if err != nil {
@ -156,6 +156,37 @@ func (v *GaugeVec) With(labels Labels) Gauge {
return g return g
} }
// CurryWith returns a vector curried with the provided labels, i.e. the
// returned vector has those labels pre-set for all labeled operations performed
// on it. The cardinality of the curried vector is reduced accordingly. The
// order of the remaining labels stays the same (just with the curried labels
// taken out of the sequence which is relevant for the
// (GetMetric)WithLabelValues methods). It is possible to curry a curried
// vector, but only with labels not yet used for currying before.
//
// The metrics contained in the GaugeVec are shared between the curried and
// uncurried vectors. They are just accessed differently. Curried and uncurried
// vectors behave identically in terms of collection. Only one must be
// registered with a given registry (usually the uncurried version). The Reset
// method deletes all metrics, even if called on a curried vector.
func (v *GaugeVec) CurryWith(labels Labels) (*GaugeVec, error) {
vec, err := v.curryWith(labels)
if vec != nil {
return &GaugeVec{vec}, err
}
return nil, err
}
// MustCurryWith works as CurryWith but panics where CurryWith would have
// returned an error.
func (v *GaugeVec) MustCurryWith(labels Labels) *GaugeVec {
vec, err := v.CurryWith(labels)
if err != nil {
panic(err)
}
return vec
}
// GaugeFunc is a Gauge whose value is determined at collect time by calling a // GaugeFunc is a Gauge whose value is determined at collect time by calling a
// provided function. // provided function.
// //

View File

@ -126,23 +126,16 @@ type HistogramOpts struct {
// string. // string.
Help string Help string
// ConstLabels are used to attach fixed labels to this // ConstLabels are used to attach fixed labels to this metric. Metrics
// Histogram. Histograms with the same fully-qualified name must have the // with the same fully-qualified name must have the same label names in
// same label names in their ConstLabels. // their ConstLabels.
// //
// Note that in most cases, labels have a value that varies during the // ConstLabels are only used rarely. In particular, do not use them to
// lifetime of a process. Those labels are usually managed with a // attach the same labels to all your metrics. Those use cases are
// HistogramVec. ConstLabels serve only special purposes. One is for the // better covered by target labels set by the scraping Prometheus
// special case where the value of a label does not change during the // server, or by one specific metric (e.g. a build_info or a
// lifetime of a process, e.g. if the revision of the running binary is // machine_role metric). See also
// put into a label. Another, more advanced purpose is if more than one // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels
// Collector needs to collect Histograms with the same fully-qualified
// name. In that case, those Summaries must differ in the values of
// their ConstLabels. See the Collector examples.
//
// If the value of a label never changes (not even between binaries),
// that label most likely should not be a label at all (but part of the
// metric name).
ConstLabels Labels ConstLabels Labels
// Buckets defines the buckets into which observations are counted. Each // Buckets defines the buckets into which observations are counted. Each
@ -322,7 +315,7 @@ func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
// example. // example.
// //
// An error is returned if the number of label values is not the same as the // An error is returned if the number of label values is not the same as the
// number of VariableLabels in Desc. // number of VariableLabels in Desc (minus any curried labels).
// //
// Note that for more than one label value, this method is prone to mistakes // Note that for more than one label value, this method is prone to mistakes
// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as // caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as
@ -345,7 +338,7 @@ func (v *HistogramVec) GetMetricWithLabelValues(lvs ...string) (Observer, error)
// are the same as for GetMetricWithLabelValues. // are the same as for GetMetricWithLabelValues.
// //
// An error is returned if the number and names of the Labels are inconsistent // An error is returned if the number and names of the Labels are inconsistent
// with those of the VariableLabels in Desc. // with those of the VariableLabels in Desc (minus any curried labels).
// //
// This method is used for the same purpose as // This method is used for the same purpose as
// GetMetricWithLabelValues(...string). See there for pros and cons of the two // GetMetricWithLabelValues(...string). See there for pros and cons of the two
@ -359,8 +352,8 @@ func (v *HistogramVec) GetMetricWith(labels Labels) (Observer, error) {
} }
// WithLabelValues works as GetMetricWithLabelValues, but panics where // WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an // GetMetricWithLabelValues would have returned an error. Not returning an
// error, WithLabelValues allows shortcuts like // error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Observe(42.21) // myVec.WithLabelValues("404", "GET").Observe(42.21)
func (v *HistogramVec) WithLabelValues(lvs ...string) Observer { func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
h, err := v.GetMetricWithLabelValues(lvs...) h, err := v.GetMetricWithLabelValues(lvs...)
@ -370,9 +363,9 @@ func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
return h return h
} }
// With works as GetMetricWith, but panics where GetMetricWithLabels would have // With works as GetMetricWith but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like // returned an error. Not returning an error allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21) // myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21)
func (v *HistogramVec) With(labels Labels) Observer { func (v *HistogramVec) With(labels Labels) Observer {
h, err := v.GetMetricWith(labels) h, err := v.GetMetricWith(labels)
if err != nil { if err != nil {
@ -381,6 +374,37 @@ func (v *HistogramVec) With(labels Labels) Observer {
return h return h
} }
// CurryWith returns a vector curried with the provided labels, i.e. the
// returned vector has those labels pre-set for all labeled operations performed
// on it. The cardinality of the curried vector is reduced accordingly. The
// order of the remaining labels stays the same (just with the curried labels
// taken out of the sequence which is relevant for the
// (GetMetric)WithLabelValues methods). It is possible to curry a curried
// vector, but only with labels not yet used for currying before.
//
// The metrics contained in the HistogramVec are shared between the curried and
// uncurried vectors. They are just accessed differently. Curried and uncurried
// vectors behave identically in terms of collection. Only one must be
// registered with a given registry (usually the uncurried version). The Reset
// method deletes all metrics, even if called on a curried vector.
func (v *HistogramVec) CurryWith(labels Labels) (*HistogramVec, error) {
vec, err := v.curryWith(labels)
if vec != nil {
return &HistogramVec{vec}, err
}
return nil, err
}
// MustCurryWith works as CurryWith but panics where CurryWith would have
// returned an error.
func (v *HistogramVec) MustCurryWith(labels Labels) *HistogramVec {
vec, err := v.CurryWith(labels)
if err != nil {
panic(err)
}
return vec
}
type constHistogram struct { type constHistogram struct {
desc *Desc desc *Desc
count uint64 count uint64

View File

@ -128,7 +128,7 @@ func TestInstrumentHandler(t *testing.T) {
} }
out.Reset() out.Reset()
if want, got := 1, len(reqCnt.children); want != got { if want, got := 1, len(reqCnt.metricMap.metrics); want != got {
t.Errorf("want %d children in reqCnt, got %d", want, got) t.Errorf("want %d children in reqCnt, got %d", want, got)
} }
cnt, err := reqCnt.GetMetricWithLabelValues("get", "418") cnt, err := reqCnt.GetMetricWithLabelValues("get", "418")

View File

@ -79,20 +79,12 @@ type Opts struct {
// with the same fully-qualified name must have the same label names in // with the same fully-qualified name must have the same label names in
// their ConstLabels. // their ConstLabels.
// //
// Note that in most cases, labels have a value that varies during the // ConstLabels are only used rarely. In particular, do not use them to
// lifetime of a process. Those labels are usually managed with a metric // attach the same labels to all your metrics. Those use cases are
// vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels // better covered by target labels set by the scraping Prometheus
// serve only special purposes. One is for the special case where the // server, or by one specific metric (e.g. a build_info or a
// value of a label does not change during the lifetime of a process, // machine_role metric). See also
// e.g. if the revision of the running binary is put into a // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels
// label. Another, more advanced purpose is if more than one Collector
// needs to collect Metrics with the same fully-qualified name. In that
// case, those Metrics must differ in the values of their
// ConstLabels. See the Collector examples.
//
// If the value of a label never changes (not even between binaries),
// that label most likely should not be a label at all (but part of the
// metric name).
ConstLabels Labels ConstLabels Labels
} }

View File

@ -159,26 +159,15 @@ func ExampleInstrumentHandlerDuration() {
[]string{"code", "method"}, []string{"code", "method"},
) )
// pushVec and pullVec are partitioned by the HTTP method and use custom // duration is partitioned by the HTTP method and handler. It uses custom
// buckets based on the expected request duration. ConstLabels are used // buckets based on the expected request duration.
// to set a handler label to mark pushVec as tracking the durations for duration := prometheus.NewHistogramVec(
// pushes and pullVec as tracking the durations for pulls. Note that prometheus.HistogramOpts{
// Name, Help, and Buckets need to be the same for consistency, so we Name: "request_duration_seconds",
// use the same HistogramOpts after just modifying the ConstLabels. Help: "A histogram of latencies for requests.",
histogramOpts := prometheus.HistogramOpts{ Buckets: []float64{.25, .5, 1, 2.5, 5, 10},
Name: "request_duration_seconds", },
Help: "A histogram of latencies for requests.", []string{"handler", "method"},
Buckets: []float64{.25, .5, 1, 2.5, 5, 10},
ConstLabels: prometheus.Labels{"handler": "push"},
}
pushVec := prometheus.NewHistogramVec(
histogramOpts,
[]string{"method"},
)
histogramOpts.ConstLabels = prometheus.Labels{"handler": "pull"}
pullVec := prometheus.NewHistogramVec(
histogramOpts,
[]string{"method"},
) )
// responseSize has no labels, making it a zero-dimensional // responseSize has no labels, making it a zero-dimensional
@ -201,23 +190,20 @@ func ExampleInstrumentHandlerDuration() {
}) })
// Register all of the metrics in the standard registry. // Register all of the metrics in the standard registry.
prometheus.MustRegister(inFlightGauge, counter, pullVec, pushVec, responseSize) prometheus.MustRegister(inFlightGauge, counter, duration, responseSize)
// Wrap the pushHandler with our shared middleware, but use the // Instrument the handlers with all the metrics, injecting the "handler"
// endpoint-specific pushVec with InstrumentHandlerDuration. // label by currying.
pushChain := InstrumentHandlerInFlight(inFlightGauge, pushChain := InstrumentHandlerInFlight(inFlightGauge,
InstrumentHandlerCounter(counter, InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "push"}),
InstrumentHandlerDuration(pushVec, InstrumentHandlerCounter(counter,
InstrumentHandlerResponseSize(responseSize, pushHandler), InstrumentHandlerResponseSize(responseSize, pushHandler),
), ),
), ),
) )
// Wrap the pushHandler with the shared middleware, but use the
// endpoint-specific pullVec with InstrumentHandlerDuration.
pullChain := InstrumentHandlerInFlight(inFlightGauge, pullChain := InstrumentHandlerInFlight(inFlightGauge,
InstrumentHandlerCounter(counter, InstrumentHandlerDuration(duration.MustCurryWith(prometheus.Labels{"handler": "pull"}),
InstrumentHandlerDuration(pullVec, InstrumentHandlerCounter(counter,
InstrumentHandlerResponseSize(responseSize, pullHandler), InstrumentHandlerResponseSize(responseSize, pullHandler),
), ),
), ),

View File

@ -101,23 +101,16 @@ type SummaryOpts struct {
// string. // string.
Help string Help string
// ConstLabels are used to attach fixed labels to this // ConstLabels are used to attach fixed labels to this metric. Metrics
// Summary. Summaries with the same fully-qualified name must have the // with the same fully-qualified name must have the same label names in
// same label names in their ConstLabels. // their ConstLabels.
// //
// Note that in most cases, labels have a value that varies during the // ConstLabels are only used rarely. In particular, do not use them to
// lifetime of a process. Those labels are usually managed with a // attach the same labels to all your metrics. Those use cases are
// SummaryVec. ConstLabels serve only special purposes. One is for the // better covered by target labels set by the scraping Prometheus
// special case where the value of a label does not change during the // server, or by one specific metric (e.g. a build_info or a
// lifetime of a process, e.g. if the revision of the running binary is // machine_role metric). See also
// put into a label. Another, more advanced purpose is if more than one // https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels
// Collector needs to collect Summaries with the same fully-qualified
// name. In that case, those Summaries must differ in the values of
// their ConstLabels. See the Collector examples.
//
// If the value of a label never changes (not even between binaries),
// that label most likely should not be a label at all (but part of the
// metric name).
ConstLabels Labels ConstLabels Labels
// Objectives defines the quantile rank estimates with their respective // Objectives defines the quantile rank estimates with their respective
@ -433,13 +426,13 @@ func NewSummaryVec(opts SummaryOpts, labelNames []string) *SummaryVec {
// //
// Keeping the Summary for later use is possible (and should be considered if // Keeping the Summary for later use is possible (and should be considered if
// performance is critical), but keep in mind that Reset, DeleteLabelValues and // performance is critical), but keep in mind that Reset, DeleteLabelValues and
// Delete can be used to delete the Summary from the SummaryVec. In that case, the // Delete can be used to delete the Summary from the SummaryVec. In that case,
// Summary will still exist, but it will not be exported anymore, even if a // the Summary will still exist, but it will not be exported anymore, even if a
// Summary with the same label values is created later. See also the CounterVec // Summary with the same label values is created later. See also the CounterVec
// example. // example.
// //
// An error is returned if the number of label values is not the same as the // An error is returned if the number of label values is not the same as the
// number of VariableLabels in Desc. // number of VariableLabels in Desc (minus any curried labels).
// //
// Note that for more than one label value, this method is prone to mistakes // Note that for more than one label value, this method is prone to mistakes
// caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as // caused by an incorrect order of arguments. Consider GetMetricWith(Labels) as
@ -462,7 +455,7 @@ func (v *SummaryVec) GetMetricWithLabelValues(lvs ...string) (Observer, error) {
// the same as for GetMetricWithLabelValues. // the same as for GetMetricWithLabelValues.
// //
// An error is returned if the number and names of the Labels are inconsistent // An error is returned if the number and names of the Labels are inconsistent
// with those of the VariableLabels in Desc. // with those of the VariableLabels in Desc (minus any curried labels).
// //
// This method is used for the same purpose as // This method is used for the same purpose as
// GetMetricWithLabelValues(...string). See there for pros and cons of the two // GetMetricWithLabelValues(...string). See there for pros and cons of the two
@ -476,8 +469,8 @@ func (v *SummaryVec) GetMetricWith(labels Labels) (Observer, error) {
} }
// WithLabelValues works as GetMetricWithLabelValues, but panics where // WithLabelValues works as GetMetricWithLabelValues, but panics where
// GetMetricWithLabelValues would have returned an error. By not returning an // GetMetricWithLabelValues would have returned an error. Not returning an
// error, WithLabelValues allows shortcuts like // error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Observe(42.21) // myVec.WithLabelValues("404", "GET").Observe(42.21)
func (v *SummaryVec) WithLabelValues(lvs ...string) Observer { func (v *SummaryVec) WithLabelValues(lvs ...string) Observer {
s, err := v.GetMetricWithLabelValues(lvs...) s, err := v.GetMetricWithLabelValues(lvs...)
@ -488,8 +481,8 @@ func (v *SummaryVec) WithLabelValues(lvs ...string) Observer {
} }
// With works as GetMetricWith, but panics where GetMetricWithLabels would have // With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like // returned an error. Not returning an error allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21) // myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21)
func (v *SummaryVec) With(labels Labels) Observer { func (v *SummaryVec) With(labels Labels) Observer {
s, err := v.GetMetricWith(labels) s, err := v.GetMetricWith(labels)
if err != nil { if err != nil {
@ -498,6 +491,37 @@ func (v *SummaryVec) With(labels Labels) Observer {
return s return s
} }
// CurryWith returns a vector curried with the provided labels, i.e. the
// returned vector has those labels pre-set for all labeled operations performed
// on it. The cardinality of the curried vector is reduced accordingly. The
// order of the remaining labels stays the same (just with the curried labels
// taken out of the sequence which is relevant for the
// (GetMetric)WithLabelValues methods). It is possible to curry a curried
// vector, but only with labels not yet used for currying before.
//
// The metrics contained in the SummaryVec are shared between the curried and
// uncurried vectors. They are just accessed differently. Curried and uncurried
// vectors behave identically in terms of collection. Only one must be
// registered with a given registry (usually the uncurried version). The Reset
// method deletes all metrics, even if called on a curried vector.
func (v *SummaryVec) CurryWith(labels Labels) (*SummaryVec, error) {
vec, err := v.curryWith(labels)
if vec != nil {
return &SummaryVec{vec}, err
}
return nil, err
}
// MustCurryWith works as CurryWith but panics where CurryWith would have
// returned an error.
func (v *SummaryVec) MustCurryWith(labels Labels) *SummaryVec {
vec, err := v.CurryWith(labels)
if err != nil {
panic(err)
}
return vec
}
type constSummary struct { type constSummary struct {
desc *Desc desc *Desc
count uint64 count uint64

View File

@ -23,72 +23,31 @@ import (
// metricVec is a Collector to bundle metrics of the same name that differ in // metricVec is a Collector to bundle metrics of the same name that differ in
// their label values. metricVec is not used directly (and therefore // their label values. metricVec is not used directly (and therefore
// unexported). It is used as a building block for implementations of vectors of // unexported). It is used as a building block for implementations of vectors of
// a given metric type, like GaugeVec, CounterVec, SummaryVec, HistogramVec, and // a given metric type, like GaugeVec, CounterVec, SummaryVec, and HistogramVec.
// UntypedVec. // It also handles label currying. It uses basicMetricVec internally.
type metricVec struct { type metricVec struct {
mtx sync.RWMutex // Protects the children. *metricMap
children map[uint64][]metricWithLabelValues
desc *Desc
newMetric func(labelValues ...string) Metric curry []curriedLabelValue
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling
// hashAdd and hashAddByte can be replaced for testing collision handling.
hashAdd func(h uint64, s string) uint64
hashAddByte func(h uint64, b byte) uint64 hashAddByte func(h uint64, b byte) uint64
} }
// newMetricVec returns an initialized metricVec. // newMetricVec returns an initialized metricVec.
func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) *metricVec { func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) *metricVec {
return &metricVec{ return &metricVec{
children: map[uint64][]metricWithLabelValues{}, metricMap: &metricMap{
desc: desc, metrics: map[uint64][]metricWithLabelValues{},
newMetric: newMetric, desc: desc,
newMetric: newMetric,
},
hashAdd: hashAdd, hashAdd: hashAdd,
hashAddByte: hashAddByte, hashAddByte: hashAddByte,
} }
} }
// metricWithLabelValues provides the metric and its label values for
// disambiguation on hash collision.
type metricWithLabelValues struct {
values []string
metric Metric
}
// Describe implements Collector. The length of the returned slice
// is always one.
func (m *metricVec) Describe(ch chan<- *Desc) {
ch <- m.desc
}
// Collect implements Collector.
func (m *metricVec) Collect(ch chan<- Metric) {
m.mtx.RLock()
defer m.mtx.RUnlock()
for _, metrics := range m.children {
for _, metric := range metrics {
ch <- metric.metric
}
}
}
func (m *metricVec) getMetricWithLabelValues(lvs ...string) (Metric, error) {
h, err := m.hashLabelValues(lvs)
if err != nil {
return nil, err
}
return m.getOrCreateMetricWithLabelValues(h, lvs), nil
}
func (m *metricVec) getMetricWith(labels Labels) (Metric, error) {
h, err := m.hashLabels(labels)
if err != nil {
return nil, err
}
return m.getOrCreateMetricWithLabels(h, labels), nil
}
// DeleteLabelValues removes the metric where the variable labels are the same // DeleteLabelValues removes the metric where the variable labels are the same
// as those passed in as labels (same order as the VariableLabels in Desc). It // as those passed in as labels (same order as the VariableLabels in Desc). It
// returns true if a metric was deleted. // returns true if a metric was deleted.
@ -105,14 +64,12 @@ func (m *metricVec) getMetricWith(labels Labels) (Metric, error) {
// 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 {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabelValues(lvs) h, err := m.hashLabelValues(lvs)
if err != nil { if err != nil {
return false return false
} }
return m.deleteByHashWithLabelValues(h, lvs)
return m.metricMap.deleteByHashWithLabelValues(h, lvs, m.curry)
} }
// Delete deletes the metric where the variable labels are the same as those // Delete deletes the metric where the variable labels are the same as those
@ -126,35 +83,190 @@ 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 {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabels(labels) h, err := m.hashLabels(labels)
if err != nil { if err != nil {
return false return false
} }
return m.deleteByHashWithLabels(h, labels) return m.metricMap.deleteByHashWithLabels(h, labels, m.curry)
}
func (m *metricVec) curryWith(labels Labels) (*metricVec, error) {
var (
newCurry []curriedLabelValue
oldCurry = m.curry
iCurry int
)
for i, label := range m.desc.variableLabels {
val, ok := labels[label]
if iCurry < len(oldCurry) && oldCurry[iCurry].index == i {
if ok {
return nil, fmt.Errorf("label name %q is already curried", label)
}
newCurry = append(newCurry, oldCurry[iCurry])
iCurry++
} else {
if !ok {
continue // Label stays uncurried.
}
newCurry = append(newCurry, curriedLabelValue{i, val})
}
}
if l := len(oldCurry) + len(labels) - len(newCurry); l > 0 {
return nil, fmt.Errorf("%d unknown label(s) found during currying", l)
}
return &metricVec{
metricMap: m.metricMap,
curry: newCurry,
hashAdd: m.hashAdd,
hashAddByte: m.hashAddByte,
}, nil
}
func (m *metricVec) getMetricWithLabelValues(lvs ...string) (Metric, error) {
h, err := m.hashLabelValues(lvs)
if err != nil {
return nil, err
}
return m.metricMap.getOrCreateMetricWithLabelValues(h, lvs, m.curry), nil
}
func (m *metricVec) getMetricWith(labels Labels) (Metric, error) {
h, err := m.hashLabels(labels)
if err != nil {
return nil, err
}
return m.metricMap.getOrCreateMetricWithLabels(h, labels, m.curry), nil
}
func (m *metricVec) hashLabelValues(vals []string) (uint64, error) {
if err := validateLabelValues(vals, len(m.desc.variableLabels)-len(m.curry)); err != nil {
return 0, err
}
var (
h = hashNew()
curry = m.curry
iVals, iCurry int
)
for i := 0; i < len(m.desc.variableLabels); i++ {
if iCurry < len(curry) && curry[iCurry].index == i {
h = m.hashAdd(h, curry[iCurry].value)
iCurry++
} else {
h = m.hashAdd(h, vals[iVals])
iVals++
}
h = m.hashAddByte(h, model.SeparatorByte)
}
return h, nil
}
func (m *metricVec) hashLabels(labels Labels) (uint64, error) {
if err := validateValuesInLabels(labels, len(m.desc.variableLabels)-len(m.curry)); err != nil {
return 0, err
}
var (
h = hashNew()
curry = m.curry
iCurry int
)
for i, label := range m.desc.variableLabels {
val, ok := labels[label]
if iCurry < len(curry) && curry[iCurry].index == i {
if ok {
return 0, fmt.Errorf("label name %q is already curried", label)
}
h = m.hashAdd(h, curry[iCurry].value)
iCurry++
} else {
if !ok {
return 0, fmt.Errorf("label name %q missing in label map", label)
}
h = m.hashAdd(h, val)
}
h = m.hashAddByte(h, model.SeparatorByte)
}
return h, nil
}
// metricWithLabelValues provides the metric and its label values for
// disambiguation on hash collision.
type metricWithLabelValues struct {
values []string
metric Metric
}
// curriedLabelValue sets the curried value for a label at the given index.
type curriedLabelValue struct {
index int
value string
}
// metricMap is a helper for metricVec and shared between differently curried
// metricVecs.
type metricMap struct {
mtx sync.RWMutex // Protects metrics.
metrics map[uint64][]metricWithLabelValues
desc *Desc
newMetric func(labelValues ...string) Metric
}
// Describe implements Collector. It will send exactly one Desc to the provided
// channel.
func (m *metricMap) Describe(ch chan<- *Desc) {
ch <- m.desc
}
// Collect implements Collector.
func (m *metricMap) Collect(ch chan<- Metric) {
m.mtx.RLock()
defer m.mtx.RUnlock()
for _, metrics := range m.metrics {
for _, metric := range metrics {
ch <- metric.metric
}
}
}
// Reset deletes all metrics in this vector.
func (m *metricMap) Reset() {
m.mtx.Lock()
defer m.mtx.Unlock()
for h := range m.metrics {
delete(m.metrics, h)
}
} }
// deleteByHashWithLabelValues removes the metric from the hash bucket h. If // deleteByHashWithLabelValues removes the metric from the hash bucket h. If
// there are multiple matches in the bucket, use lvs to select a metric and // there are multiple matches in the bucket, use lvs to select a metric and
// remove only that metric. // remove only that metric.
func (m *metricVec) deleteByHashWithLabelValues(h uint64, lvs []string) bool { func (m *metricMap) deleteByHashWithLabelValues(
metrics, ok := m.children[h] h uint64, lvs []string, curry []curriedLabelValue,
) bool {
m.mtx.Lock()
defer m.mtx.Unlock()
metrics, ok := m.metrics[h]
if !ok { if !ok {
return false return false
} }
i := m.findMetricWithLabelValues(metrics, lvs) i := findMetricWithLabelValues(metrics, lvs, curry)
if i >= len(metrics) { if i >= len(metrics) {
return false return false
} }
if len(metrics) > 1 { if len(metrics) > 1 {
m.children[h] = append(metrics[:i], metrics[i+1:]...) m.metrics[h] = append(metrics[:i], metrics[i+1:]...)
} else { } else {
delete(m.children, h) delete(m.metrics, h)
} }
return true return true
} }
@ -162,71 +274,35 @@ func (m *metricVec) deleteByHashWithLabelValues(h uint64, lvs []string) bool {
// deleteByHashWithLabels removes the metric from the hash bucket h. If there // deleteByHashWithLabels removes the metric from the hash bucket h. If there
// are multiple matches in the bucket, use lvs to select a metric and remove // are multiple matches in the bucket, use lvs to select a metric and remove
// only that metric. // only that metric.
func (m *metricVec) deleteByHashWithLabels(h uint64, labels Labels) bool { func (m *metricMap) deleteByHashWithLabels(
metrics, ok := m.children[h] h uint64, labels Labels, curry []curriedLabelValue,
) bool {
metrics, ok := m.metrics[h]
if !ok { if !ok {
return false return false
} }
i := m.findMetricWithLabels(metrics, labels) i := findMetricWithLabels(m.desc, metrics, labels, curry)
if i >= len(metrics) { if i >= len(metrics) {
return false return false
} }
if len(metrics) > 1 { if len(metrics) > 1 {
m.children[h] = append(metrics[:i], metrics[i+1:]...) m.metrics[h] = append(metrics[:i], metrics[i+1:]...)
} else { } else {
delete(m.children, h) delete(m.metrics, h)
} }
return true return true
} }
// Reset deletes all metrics in this vector.
func (m *metricVec) Reset() {
m.mtx.Lock()
defer m.mtx.Unlock()
for h := range m.children {
delete(m.children, h)
}
}
func (m *metricVec) hashLabelValues(vals []string) (uint64, error) {
if err := validateLabelValues(vals, len(m.desc.variableLabels)); err != nil {
return 0, err
}
h := hashNew()
for _, val := range vals {
h = m.hashAdd(h, val)
h = m.hashAddByte(h, model.SeparatorByte)
}
return h, nil
}
func (m *metricVec) hashLabels(labels Labels) (uint64, error) {
if err := validateValuesInLabels(labels, len(m.desc.variableLabels)); err != nil {
return 0, err
}
h := hashNew()
for _, label := range m.desc.variableLabels {
val, ok := labels[label]
if !ok {
return 0, fmt.Errorf("label name %q missing in label map", label)
}
h = m.hashAdd(h, val)
h = m.hashAddByte(h, model.SeparatorByte)
}
return h, nil
}
// getOrCreateMetricWithLabelValues retrieves the metric by hash and label value // getOrCreateMetricWithLabelValues retrieves the metric by hash and label value
// or creates it and returns the new one. // or creates it and returns the new one.
// //
// This function holds the mutex. // This function holds the mutex.
func (m *metricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string) Metric { func (m *metricMap) getOrCreateMetricWithLabelValues(
hash uint64, lvs []string, curry []curriedLabelValue,
) Metric {
m.mtx.RLock() m.mtx.RLock()
metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs) metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs, curry)
m.mtx.RUnlock() m.mtx.RUnlock()
if ok { if ok {
return metric return metric
@ -234,13 +310,11 @@ func (m *metricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string)
m.mtx.Lock() m.mtx.Lock()
defer m.mtx.Unlock() defer m.mtx.Unlock()
metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs) metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs, curry)
if !ok { if !ok {
// Copy to avoid allocation in case wo don't go down this code path. inlinedLVs := inlineLabelValues(lvs, curry)
copiedLVs := make([]string, len(lvs)) metric = m.newMetric(inlinedLVs...)
copy(copiedLVs, lvs) m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: inlinedLVs, metric: metric})
metric = m.newMetric(copiedLVs...)
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: copiedLVs, metric: metric})
} }
return metric return metric
} }
@ -249,9 +323,11 @@ func (m *metricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string)
// or creates it and returns the new one. // or creates it and returns the new one.
// //
// This function holds the mutex. // This function holds the mutex.
func (m *metricVec) getOrCreateMetricWithLabels(hash uint64, labels Labels) Metric { func (m *metricMap) getOrCreateMetricWithLabels(
hash uint64, labels Labels, curry []curriedLabelValue,
) Metric {
m.mtx.RLock() m.mtx.RLock()
metric, ok := m.getMetricWithHashAndLabels(hash, labels) metric, ok := m.getMetricWithHashAndLabels(hash, labels, curry)
m.mtx.RUnlock() m.mtx.RUnlock()
if ok { if ok {
return metric return metric
@ -259,21 +335,23 @@ func (m *metricVec) getOrCreateMetricWithLabels(hash uint64, labels Labels) Metr
m.mtx.Lock() m.mtx.Lock()
defer m.mtx.Unlock() defer m.mtx.Unlock()
metric, ok = m.getMetricWithHashAndLabels(hash, labels) metric, ok = m.getMetricWithHashAndLabels(hash, labels, curry)
if !ok { if !ok {
lvs := m.extractLabelValues(labels) lvs := extractLabelValues(m.desc, labels, curry)
metric = m.newMetric(lvs...) metric = m.newMetric(lvs...)
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: lvs, metric: metric}) m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: lvs, metric: metric})
} }
return metric return metric
} }
// getMetricWithHashAndLabelValues gets a metric while handling possible // getMetricWithHashAndLabelValues gets a metric while handling possible
// collisions in the hash space. Must be called while holding the read mutex. // collisions in the hash space. Must be called while holding the read mutex.
func (m *metricVec) getMetricWithHashAndLabelValues(h uint64, lvs []string) (Metric, bool) { func (m *metricMap) getMetricWithHashAndLabelValues(
metrics, ok := m.children[h] h uint64, lvs []string, curry []curriedLabelValue,
) (Metric, bool) {
metrics, ok := m.metrics[h]
if ok { if ok {
if i := m.findMetricWithLabelValues(metrics, lvs); i < len(metrics) { if i := findMetricWithLabelValues(metrics, lvs, curry); i < len(metrics) {
return metrics[i].metric, true return metrics[i].metric, true
} }
} }
@ -282,10 +360,12 @@ func (m *metricVec) getMetricWithHashAndLabelValues(h uint64, lvs []string) (Met
// getMetricWithHashAndLabels gets a metric while handling possible collisions in // getMetricWithHashAndLabels gets a metric while handling possible collisions in
// the hash space. Must be called while holding read mutex. // the hash space. Must be called while holding read mutex.
func (m *metricVec) getMetricWithHashAndLabels(h uint64, labels Labels) (Metric, bool) { func (m *metricMap) getMetricWithHashAndLabels(
metrics, ok := m.children[h] h uint64, labels Labels, curry []curriedLabelValue,
) (Metric, bool) {
metrics, ok := m.metrics[h]
if ok { if ok {
if i := m.findMetricWithLabels(metrics, labels); i < len(metrics) { if i := findMetricWithLabels(m.desc, metrics, labels, curry); i < len(metrics) {
return metrics[i].metric, true return metrics[i].metric, true
} }
} }
@ -294,9 +374,11 @@ func (m *metricVec) getMetricWithHashAndLabels(h uint64, labels Labels) (Metric,
// findMetricWithLabelValues returns the index of the matching metric or // findMetricWithLabelValues returns the index of the matching metric or
// len(metrics) if not found. // len(metrics) if not found.
func (m *metricVec) findMetricWithLabelValues(metrics []metricWithLabelValues, lvs []string) int { func findMetricWithLabelValues(
metrics []metricWithLabelValues, lvs []string, curry []curriedLabelValue,
) int {
for i, metric := range metrics { for i, metric := range metrics {
if m.matchLabelValues(metric.values, lvs) { if matchLabelValues(metric.values, lvs, curry) {
return i return i
} }
} }
@ -305,32 +387,51 @@ func (m *metricVec) findMetricWithLabelValues(metrics []metricWithLabelValues, l
// findMetricWithLabels returns the index of the matching metric or len(metrics) // findMetricWithLabels returns the index of the matching metric or len(metrics)
// if not found. // if not found.
func (m *metricVec) findMetricWithLabels(metrics []metricWithLabelValues, labels Labels) int { func findMetricWithLabels(
desc *Desc, metrics []metricWithLabelValues, labels Labels, curry []curriedLabelValue,
) int {
for i, metric := range metrics { for i, metric := range metrics {
if m.matchLabels(metric.values, labels) { if matchLabels(desc, metric.values, labels, curry) {
return i return i
} }
} }
return len(metrics) return len(metrics)
} }
func (m *metricVec) matchLabelValues(values []string, lvs []string) bool { func matchLabelValues(values []string, lvs []string, curry []curriedLabelValue) bool {
if len(values) != len(lvs) { if len(values) != len(lvs)+len(curry) {
return false return false
} }
var iLVs, iCurry int
for i, v := range values { for i, v := range values {
if v != lvs[i] { if iCurry < len(curry) && curry[iCurry].index == i {
if v != curry[iCurry].value {
return false
}
iCurry++
continue
}
if v != lvs[iLVs] {
return false return false
} }
iLVs++
} }
return true return true
} }
func (m *metricVec) matchLabels(values []string, labels Labels) bool { func matchLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool {
if len(labels) != len(values) { if len(values) != len(labels)+len(curry) {
return false return false
} }
for i, k := range m.desc.variableLabels { iCurry := 0
for i, k := range desc.variableLabels {
if iCurry < len(curry) && curry[iCurry].index == i {
if values[i] != curry[iCurry].value {
return false
}
iCurry++
continue
}
if values[i] != labels[k] { if values[i] != labels[k] {
return false return false
} }
@ -338,10 +439,31 @@ func (m *metricVec) matchLabels(values []string, labels Labels) bool {
return true return true
} }
func (m *metricVec) extractLabelValues(labels Labels) []string { func extractLabelValues(desc *Desc, labels Labels, curry []curriedLabelValue) []string {
labelValues := make([]string, len(labels)) labelValues := make([]string, len(labels)+len(curry))
for i, k := range m.desc.variableLabels { iCurry := 0
for i, k := range desc.variableLabels {
if iCurry < len(curry) && curry[iCurry].index == i {
labelValues[i] = curry[iCurry].value
iCurry++
continue
}
labelValues[i] = labels[k] labelValues[i] = labels[k]
} }
return labelValues return labelValues
} }
func inlineLabelValues(lvs []string, curry []curriedLabelValue) []string {
labelValues := make([]string, len(lvs)+len(curry))
var iCurry, iLVs int
for i := range labelValues {
if iCurry < len(curry) && curry[iCurry].index == i {
labelValues[i] = curry[iCurry].value
iCurry++
continue
}
labelValues[i] = lvs[iLVs]
iLVs++
}
return labelValues
}

View File

@ -166,7 +166,7 @@ func testMetricVec(t *testing.T, vec *GaugeVec) {
} }
var total int var total int
for _, metrics := range vec.children { for _, metrics := range vec.metricMap.metrics {
for _, metric := range metrics { for _, metric := range metrics {
total++ total++
copy(pair[:], metric.values) copy(pair[:], metric.values)
@ -201,7 +201,7 @@ func testMetricVec(t *testing.T, vec *GaugeVec) {
vec.Reset() vec.Reset()
if len(vec.children) > 0 { if len(vec.metricMap.metrics) > 0 {
t.Fatalf("reset failed") t.Fatalf("reset failed")
} }
} }
@ -239,6 +239,229 @@ func TestCounterVecEndToEndWithCollision(t *testing.T) {
} }
} }
func TestCurryVec(t *testing.T) {
vec := NewCounterVec(
CounterOpts{
Name: "test",
Help: "helpless",
},
[]string{"one", "two", "three"},
)
testCurryVec(t, vec)
}
func TestCurryVecWithCollisions(t *testing.T) {
vec := NewCounterVec(
CounterOpts{
Name: "test",
Help: "helpless",
},
[]string{"one", "two", "three"},
)
vec.hashAdd = func(h uint64, s string) uint64 { return 1 }
vec.hashAddByte = func(h uint64, b byte) uint64 { return 1 }
testCurryVec(t, vec)
}
func testCurryVec(t *testing.T, vec *CounterVec) {
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)
}
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)
}
}
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", 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 unexpectedly succeeded")
}
if c2.DeleteLabelValues("1", "2") {
t.Error("deletion unexpectedly succeeded")
}
if !c1.Delete(Labels{"two": "2", "one": "1"}) {
t.Error("deletion failed")
}
if !c2.DeleteLabelValues("11", "22") {
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"},