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.
//
// 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
// 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.
//
// 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
// 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
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// GetMetricWithLabelValues would have returned an error. Not returning an
// error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42)
func (v *CounterVec) WithLabelValues(lvs ...string) Counter {
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
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
// returned an error. Not returning an error allows shortcuts like
// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Add(42)
func (v *CounterVec) With(labels Labels) Counter {
c, err := v.GetMetricWith(labels)
if err != nil {
@ -170,6 +170,37 @@ func (v *CounterVec) With(labels Labels) Counter {
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
// provided function.
//

View File

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

View File

@ -98,7 +98,7 @@ func NewGaugeVec(opts GaugeOpts, labelNames []string) *GaugeVec {
// example.
//
// 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
// 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.
//
// 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
// 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
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// GetMetricWithLabelValues would have returned an error. Not returning an
// error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Add(42)
func (v *GaugeVec) WithLabelValues(lvs ...string) Gauge {
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
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Add(42)
// returned an error. Not returning an error allows shortcuts like
// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Add(42)
func (v *GaugeVec) With(labels Labels) Gauge {
g, err := v.GetMetricWith(labels)
if err != nil {
@ -156,6 +156,37 @@ func (v *GaugeVec) With(labels Labels) Gauge {
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
// provided function.
//

View File

@ -126,23 +126,16 @@ type HistogramOpts struct {
// string.
Help string
// ConstLabels are used to attach fixed labels to this
// Histogram. Histograms with the same fully-qualified name must have the
// same label names in their ConstLabels.
// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.
//
// Note that in most cases, labels have a value that varies during the
// lifetime of a process. Those labels are usually managed with a
// HistogramVec. ConstLabels serve only special purposes. One is for the
// special case where the value of a label does not change during the
// lifetime of a process, e.g. if the revision of the running binary is
// put into a label. Another, more advanced purpose is if more than one
// 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 are only used rarely. In particular, do not use them to
// attach the same labels to all your metrics. Those use cases are
// better covered by target labels set by the scraping Prometheus
// server, or by one specific metric (e.g. a build_info or a
// machine_role metric). See also
// https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels
ConstLabels Labels
// Buckets defines the buckets into which observations are counted. Each
@ -322,7 +315,7 @@ func NewHistogramVec(opts HistogramOpts, labelNames []string) *HistogramVec {
// example.
//
// 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
// 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.
//
// 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
// 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
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// GetMetricWithLabelValues would have returned an error. Not returning an
// error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Observe(42.21)
func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
h, err := v.GetMetricWithLabelValues(lvs...)
@ -370,9 +363,9 @@ func (v *HistogramVec) WithLabelValues(lvs ...string) Observer {
return h
}
// With works as GetMetricWith, but panics where GetMetricWithLabels would have
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21)
// With works as GetMetricWith but panics where GetMetricWithLabels would have
// returned an error. Not returning an error allows shortcuts like
// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21)
func (v *HistogramVec) With(labels Labels) Observer {
h, err := v.GetMetricWith(labels)
if err != nil {
@ -381,6 +374,37 @@ func (v *HistogramVec) With(labels Labels) Observer {
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 {
desc *Desc
count uint64

View File

@ -128,7 +128,7 @@ func TestInstrumentHandler(t *testing.T) {
}
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)
}
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
// their ConstLabels.
//
// Note that in most cases, labels have a value that varies during the
// lifetime of a process. Those labels are usually managed with a metric
// vector collector (like CounterVec, GaugeVec, UntypedVec). ConstLabels
// serve only special purposes. One is for the special case where the
// value of a label does not change during the lifetime of a process,
// e.g. if the revision of the running binary is put into a
// 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 are only used rarely. In particular, do not use them to
// attach the same labels to all your metrics. Those use cases are
// better covered by target labels set by the scraping Prometheus
// server, or by one specific metric (e.g. a build_info or a
// machine_role metric). See also
// https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels
ConstLabels Labels
}

View File

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

View File

@ -101,23 +101,16 @@ type SummaryOpts struct {
// string.
Help string
// ConstLabels are used to attach fixed labels to this
// Summary. Summaries with the same fully-qualified name must have the
// same label names in their ConstLabels.
// ConstLabels are used to attach fixed labels to this metric. Metrics
// with the same fully-qualified name must have the same label names in
// their ConstLabels.
//
// Note that in most cases, labels have a value that varies during the
// lifetime of a process. Those labels are usually managed with a
// SummaryVec. ConstLabels serve only special purposes. One is for the
// special case where the value of a label does not change during the
// lifetime of a process, e.g. if the revision of the running binary is
// put into a label. Another, more advanced purpose is if more than one
// 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 are only used rarely. In particular, do not use them to
// attach the same labels to all your metrics. Those use cases are
// better covered by target labels set by the scraping Prometheus
// server, or by one specific metric (e.g. a build_info or a
// machine_role metric). See also
// https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels,-not-static-scraped-labels
ConstLabels Labels
// 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
// 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
// Summary will still exist, but it will not be exported anymore, even if a
// Delete can be used to delete the Summary from the SummaryVec. In that case,
// 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
// example.
//
// 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
// 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.
//
// 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
// 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
// GetMetricWithLabelValues would have returned an error. By not returning an
// error, WithLabelValues allows shortcuts like
// GetMetricWithLabelValues would have returned an error. Not returning an
// error allows shortcuts like
// myVec.WithLabelValues("404", "GET").Observe(42.21)
func (v *SummaryVec) WithLabelValues(lvs ...string) Observer {
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
// returned an error. By not returning an error, With allows shortcuts like
// myVec.With(Labels{"code": "404", "method": "GET"}).Observe(42.21)
// returned an error. Not returning an error allows shortcuts like
// myVec.With(prometheus.Labels{"code": "404", "method": "GET"}).Observe(42.21)
func (v *SummaryVec) With(labels Labels) Observer {
s, err := v.GetMetricWith(labels)
if err != nil {
@ -498,6 +491,37 @@ func (v *SummaryVec) With(labels Labels) Observer {
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 {
desc *Desc
count uint64

View File

@ -23,72 +23,31 @@ import (
// metricVec is a Collector to bundle metrics of the same name that differ in
// their label values. metricVec is not used directly (and therefore
// unexported). It is used as a building block for implementations of vectors of
// a given metric type, like GaugeVec, CounterVec, SummaryVec, HistogramVec, and
// UntypedVec.
// a given metric type, like GaugeVec, CounterVec, SummaryVec, and HistogramVec.
// It also handles label currying. It uses basicMetricVec internally.
type metricVec struct {
mtx sync.RWMutex // Protects the children.
children map[uint64][]metricWithLabelValues
desc *Desc
*metricMap
newMetric func(labelValues ...string) Metric
hashAdd func(h uint64, s string) uint64 // replace hash function for testing collision handling
curry []curriedLabelValue
// hashAdd and hashAddByte can be replaced for testing collision handling.
hashAdd func(h uint64, s string) uint64
hashAddByte func(h uint64, b byte) uint64
}
// newMetricVec returns an initialized metricVec.
func newMetricVec(desc *Desc, newMetric func(lvs ...string) Metric) *metricVec {
return &metricVec{
children: map[uint64][]metricWithLabelValues{},
desc: desc,
newMetric: newMetric,
metricMap: &metricMap{
metrics: map[uint64][]metricWithLabelValues{},
desc: desc,
newMetric: newMetric,
},
hashAdd: hashAdd,
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
// as those passed in as labels (same order as the VariableLabels in Desc). It
// 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).
// See also the CounterVec example.
func (m *metricVec) DeleteLabelValues(lvs ...string) bool {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabelValues(lvs)
if err != nil {
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
@ -126,35 +83,190 @@ func (m *metricVec) DeleteLabelValues(lvs ...string) bool {
// This method is used for the same purpose as DeleteLabelValues(...string). See
// there for pros and cons of the two methods.
func (m *metricVec) Delete(labels Labels) bool {
m.mtx.Lock()
defer m.mtx.Unlock()
h, err := m.hashLabels(labels)
if err != nil {
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
// there are multiple matches in the bucket, use lvs to select a metric and
// remove only that metric.
func (m *metricVec) deleteByHashWithLabelValues(h uint64, lvs []string) bool {
metrics, ok := m.children[h]
func (m *metricMap) deleteByHashWithLabelValues(
h uint64, lvs []string, curry []curriedLabelValue,
) bool {
m.mtx.Lock()
defer m.mtx.Unlock()
metrics, ok := m.metrics[h]
if !ok {
return false
}
i := m.findMetricWithLabelValues(metrics, lvs)
i := findMetricWithLabelValues(metrics, lvs, curry)
if i >= len(metrics) {
return false
}
if len(metrics) > 1 {
m.children[h] = append(metrics[:i], metrics[i+1:]...)
m.metrics[h] = append(metrics[:i], metrics[i+1:]...)
} else {
delete(m.children, h)
delete(m.metrics, h)
}
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
// are multiple matches in the bucket, use lvs to select a metric and remove
// only that metric.
func (m *metricVec) deleteByHashWithLabels(h uint64, labels Labels) bool {
metrics, ok := m.children[h]
func (m *metricMap) deleteByHashWithLabels(
h uint64, labels Labels, curry []curriedLabelValue,
) bool {
metrics, ok := m.metrics[h]
if !ok {
return false
}
i := m.findMetricWithLabels(metrics, labels)
i := findMetricWithLabels(m.desc, metrics, labels, curry)
if i >= len(metrics) {
return false
}
if len(metrics) > 1 {
m.children[h] = append(metrics[:i], metrics[i+1:]...)
m.metrics[h] = append(metrics[:i], metrics[i+1:]...)
} else {
delete(m.children, h)
delete(m.metrics, h)
}
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
// or creates it and returns the new one.
//
// 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()
metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs)
metric, ok := m.getMetricWithHashAndLabelValues(hash, lvs, curry)
m.mtx.RUnlock()
if ok {
return metric
@ -234,13 +310,11 @@ func (m *metricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string)
m.mtx.Lock()
defer m.mtx.Unlock()
metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs)
metric, ok = m.getMetricWithHashAndLabelValues(hash, lvs, curry)
if !ok {
// Copy to avoid allocation in case wo don't go down this code path.
copiedLVs := make([]string, len(lvs))
copy(copiedLVs, lvs)
metric = m.newMetric(copiedLVs...)
m.children[hash] = append(m.children[hash], metricWithLabelValues{values: copiedLVs, metric: metric})
inlinedLVs := inlineLabelValues(lvs, curry)
metric = m.newMetric(inlinedLVs...)
m.metrics[hash] = append(m.metrics[hash], metricWithLabelValues{values: inlinedLVs, metric: metric})
}
return metric
}
@ -249,9 +323,11 @@ func (m *metricVec) getOrCreateMetricWithLabelValues(hash uint64, lvs []string)
// or creates it and returns the new one.
//
// 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()
metric, ok := m.getMetricWithHashAndLabels(hash, labels)
metric, ok := m.getMetricWithHashAndLabels(hash, labels, curry)
m.mtx.RUnlock()
if ok {
return metric
@ -259,21 +335,23 @@ func (m *metricVec) getOrCreateMetricWithLabels(hash uint64, labels Labels) Metr
m.mtx.Lock()
defer m.mtx.Unlock()
metric, ok = m.getMetricWithHashAndLabels(hash, labels)
metric, ok = m.getMetricWithHashAndLabels(hash, labels, curry)
if !ok {
lvs := m.extractLabelValues(labels)
lvs := extractLabelValues(m.desc, labels, curry)
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
}
// getMetricWithHashAndLabelValues gets a metric while handling possible
// collisions in the hash space. Must be called while holding the read mutex.
func (m *metricVec) getMetricWithHashAndLabelValues(h uint64, lvs []string) (Metric, bool) {
metrics, ok := m.children[h]
func (m *metricMap) getMetricWithHashAndLabelValues(
h uint64, lvs []string, curry []curriedLabelValue,
) (Metric, bool) {
metrics, ok := m.metrics[h]
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
}
}
@ -282,10 +360,12 @@ func (m *metricVec) getMetricWithHashAndLabelValues(h uint64, lvs []string) (Met
// getMetricWithHashAndLabels gets a metric while handling possible collisions in
// the hash space. Must be called while holding read mutex.
func (m *metricVec) getMetricWithHashAndLabels(h uint64, labels Labels) (Metric, bool) {
metrics, ok := m.children[h]
func (m *metricMap) getMetricWithHashAndLabels(
h uint64, labels Labels, curry []curriedLabelValue,
) (Metric, bool) {
metrics, ok := m.metrics[h]
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
}
}
@ -294,9 +374,11 @@ func (m *metricVec) getMetricWithHashAndLabels(h uint64, labels Labels) (Metric,
// findMetricWithLabelValues returns the index of the matching metric or
// 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 {
if m.matchLabelValues(metric.values, lvs) {
if matchLabelValues(metric.values, lvs, curry) {
return i
}
}
@ -305,32 +387,51 @@ func (m *metricVec) findMetricWithLabelValues(metrics []metricWithLabelValues, l
// findMetricWithLabels returns the index of the matching metric or len(metrics)
// 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 {
if m.matchLabels(metric.values, labels) {
if matchLabels(desc, metric.values, labels, curry) {
return i
}
}
return len(metrics)
}
func (m *metricVec) matchLabelValues(values []string, lvs []string) bool {
if len(values) != len(lvs) {
func matchLabelValues(values []string, lvs []string, curry []curriedLabelValue) bool {
if len(values) != len(lvs)+len(curry) {
return false
}
var iLVs, iCurry int
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
}
iLVs++
}
return true
}
func (m *metricVec) matchLabels(values []string, labels Labels) bool {
if len(labels) != len(values) {
func matchLabels(desc *Desc, values []string, labels Labels, curry []curriedLabelValue) bool {
if len(values) != len(labels)+len(curry) {
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] {
return false
}
@ -338,10 +439,31 @@ func (m *metricVec) matchLabels(values []string, labels Labels) bool {
return true
}
func (m *metricVec) extractLabelValues(labels Labels) []string {
labelValues := make([]string, len(labels))
for i, k := range m.desc.variableLabels {
func extractLabelValues(desc *Desc, labels Labels, curry []curriedLabelValue) []string {
labelValues := make([]string, len(labels)+len(curry))
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]
}
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
for _, metrics := range vec.children {
for _, metrics := range vec.metricMap.metrics {
for _, metric := range metrics {
total++
copy(pair[:], metric.values)
@ -201,7 +201,7 @@ func testMetricVec(t *testing.T, vec *GaugeVec) {
vec.Reset()
if len(vec.children) > 0 {
if len(vec.metricMap.metrics) > 0 {
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) {
benchmarkMetricVecWithLabelValues(b, map[string][]string{
"l1": {"onevalue"},