From f9401ffab9207bac22b8dd8a8d3ae149472289fc Mon Sep 17 00:00:00 2001 From: Bjoern Rabenstein Date: Mon, 23 Jun 2014 14:15:35 +0200 Subject: [PATCH] Added "callback" metrics, e.g. GaugeFunc. Change-Id: I449b558207963ce60572bd04c8102f1db684dd4c --- prometheus/counter.go | 26 +++++++++++++++ prometheus/example_selfcollector_test.go | 7 ++-- prometheus/examples_test.go | 19 +++++++++++ prometheus/gauge.go | 24 ++++++++++++++ prometheus/gauge_test.go | 24 ++++++++++++++ prometheus/untyped.go | 24 ++++++++++++++ prometheus/value.go | 41 +++++++++++++++++++++++- 7 files changed, 161 insertions(+), 4 deletions(-) diff --git a/prometheus/counter.go b/prometheus/counter.go index 68057a5..6a729a3 100644 --- a/prometheus/counter.go +++ b/prometheus/counter.go @@ -147,3 +147,29 @@ func (m *CounterVec) WithLabelValues(lvs ...string) Counter { func (m *CounterVec) With(labels Labels) Counter { return m.MetricVec.With(labels).(Counter) } + +// CounterFunc is a Counter whose value is determined at collect time by calling a +// provided function. +// +// To create CounterFunc instances, use NewCounterFunc. +type CounterFunc interface { + Metric + Collector +} + +// NewCounterFunc creates a new CounterFunc based on the provided +// CounterOpts. The value reported is determined by calling the given function +// from within the Write method. Take into account that metric collection may +// happen concurrently. If that results in concurrent calls to Write, like in +// the case where a CounterFunc is directly registered with Prometheus, the +// provided function must be concurrency-safe. The function should also honor +// the contract for a Counter (values only go up, not down), but compliance will +// not be checked. +func NewCounterFunc(opts CounterOpts, function func() float64) CounterFunc { + return newValueFunc(NewDesc( + BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), + opts.Help, + nil, + opts.ConstLabels, + ), CounterValue, function) +} diff --git a/prometheus/example_selfcollector_test.go b/prometheus/example_selfcollector_test.go index 39d09c6..e4fef39 100644 --- a/prometheus/example_selfcollector_test.go +++ b/prometheus/example_selfcollector_test.go @@ -29,14 +29,15 @@ func NewCallbackMetric(desc *prometheus.Desc, callback func() float64) *Callback return result } +// TODO: Come up with a better example. + // CallbackMetric is an example for a user-defined Metric that exports the // result of a function call as a metric of type "untyped" without any // labels. It uses SelfCollector to turn the Metric into a Collector so that it // can be registered with Prometheus. // -// Note that this is a pretty low-level approach. A more high-level approach is -// to implement a Collector directly and not an individual Metric, see the -// Collector examples. +// Note that this example is pretty much academic as the prometheus package +// already provides an UntypedFunc type. type CallbackMetric struct { prometheus.SelfCollector diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 5d2eefa..f9b1299 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -18,6 +18,7 @@ import ( "fmt" "math" "net/http" + "runtime" "sort" dto "github.com/prometheus/client_model/go" @@ -72,6 +73,24 @@ func ExampleGaugeVec() { opsQueued.With(prometheus.Labels{"type": "delete", "user": "alice"}).Inc() } +func ExampleGaugeFunc() { + if _, err := prometheus.Register(prometheus.NewGaugeFunc( + prometheus.GaugeOpts{ + Subsystem: "runtime", + Name: "goroutines_count", + Help: "Number of goroutines that currently exist.", + }, + func() float64 { return float64(runtime.NumGoroutine()) }, + )); err == nil { + fmt.Println("GaugeFunc 'goroutines_count' registered.\n") + } + // Note that the count of goroutines is a gauge (and not a counter) as + // it can go up and down. + + // Output: + // GaugeFunc 'goroutines_count' registered. +} + func ExampleCounter() { pushCounter := prometheus.NewCounter(prometheus.CounterOpts{ Name: "repository_pushes", // Note: No help string... diff --git a/prometheus/gauge.go b/prometheus/gauge.go index 1c8db9b..e1d3cc2 100644 --- a/prometheus/gauge.go +++ b/prometheus/gauge.go @@ -121,3 +121,27 @@ func (m *GaugeVec) WithLabelValues(lvs ...string) Gauge { func (m *GaugeVec) With(labels Labels) Gauge { return m.MetricVec.With(labels).(Gauge) } + +// GaugeFunc is a Gauge whose value is determined at collect time by calling a +// provided function. +// +// To create GaugeFunc instances, use NewGaugeFunc. +type GaugeFunc interface { + Metric + Collector +} + +// NewGaugeFunc creates a new GaugeFunc based on the provided GaugeOpts. The +// value reported is determined by calling the given function from within the +// Write method. Take into account that metric collection may happen +// concurrently. If that results in concurrent calls to Write, like in the case +// where a GaugeFunc is directly registered with Prometheus, the provided +// function must be concurrency-safe. +func NewGaugeFunc(opts GaugeOpts, function func() float64) GaugeFunc { + return newValueFunc(NewDesc( + BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), + opts.Help, + nil, + opts.ConstLabels, + ), GaugeValue, function) +} diff --git a/prometheus/gauge_test.go b/prometheus/gauge_test.go index 6f3679a..86c5a6e 100644 --- a/prometheus/gauge_test.go +++ b/prometheus/gauge_test.go @@ -19,6 +19,8 @@ import ( "sync" "testing" "testing/quick" + + dto "github.com/prometheus/client_model/go" ) func listenGaugeStream(vals, result chan float64, done chan struct{}) { @@ -156,3 +158,25 @@ func TestGaugeVecConcurrency(t *testing.T) { t.Fatal(err) } } + +func TestGaugeFunc(t *testing.T) { + gf := NewGaugeFunc( + GaugeOpts{ + Name: "test_name", + Help: "test help", + ConstLabels: Labels{"a": "1", "b": "2"}, + }, + func() float64 { return 3.1415 }, + ) + + if expected, got := `Desc{fqName: "test_name", help: "test help", constLabels: {a="1",b="2"}, variableLabels: []}`, gf.Desc().String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } + + m := &dto.Metric{} + gf.Write(m) + + if expected, got := `label: label: gauge: `, m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} diff --git a/prometheus/untyped.go b/prometheus/untyped.go index 14388ef..d3a78fc 100644 --- a/prometheus/untyped.go +++ b/prometheus/untyped.go @@ -119,3 +119,27 @@ func (m *UntypedVec) WithLabelValues(lvs ...string) Untyped { func (m *UntypedVec) With(labels Labels) Untyped { return m.MetricVec.With(labels).(Untyped) } + +// UntypedFunc is an Untyped whose value is determined at collect time by +// calling a provided function. +// +// To create UntypedFunc instances, use NewUntypedFunc. +type UntypedFunc interface { + Metric + Collector +} + +// NewUntypedFunc creates a new UntypedFunc based on the provided +// UntypedOpts. The value reported is determined by calling the given function +// from within the Write method. Take into account that metric collection may +// happen concurrently. If that results in concurrent calls to Write, like in +// the case where an UntypedFunc is directly registered with Prometheus, the +// provided function must be concurrency-safe. +func NewUntypedFunc(opts UntypedOpts, function func() float64) UntypedFunc { + return newValueFunc(NewDesc( + BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), + opts.Help, + nil, + opts.ConstLabels, + ), UntypedValue, function) +} diff --git a/prometheus/value.go b/prometheus/value.go index 7dbdcee..70af308 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -51,7 +51,7 @@ type value struct { labelPairs []*dto.LabelPair } -// newValue returns a newly allocated Value with the given Desc, ValueType, +// newValue returns a newly allocated value with the given Desc, ValueType, // sample value and label values. It panics if the number of label // values is different from the number of variable labels in Desc. func newValue(desc *Desc, valueType ValueType, val float64, labelValues ...string) *value { @@ -106,6 +106,45 @@ func (v *value) Write(out *dto.Metric) { populateMetric(v.valType, val, v.labelPairs, out) } +// valueFunc is a generic metric for simple values retrieved on collect time +// from a function. It implements Metric and Collector. Its effective type is +// determined by ValueType. This is a low-level building block used by the +// library to back the implementations of CounterFunc, GaugeFunc, and +// UntypedFunc. +type valueFunc struct { + SelfCollector + + desc *Desc + valType ValueType + function func() float64 + labelPairs []*dto.LabelPair +} + +// newValueFunc returns a newly allocated valueFunc with the given Desc and +// ValueType. The value reported is determined by calling the given function +// from within the Write method. Take into account that metric collection may +// happen concurrently. If that results in concurrent calls to Write, like in +// the case where a valueFunc is directly registered with Prometheus, the +// provided function must be concurrency-safe. +func newValueFunc(desc *Desc, valueType ValueType, function func() float64) *valueFunc { + result := &valueFunc{ + desc: desc, + valType: valueType, + function: function, + labelPairs: makeLabelPairs(desc, nil), + } + result.Init(result) + return result +} + +func (v *valueFunc) Desc() *Desc { + return v.desc +} + +func (v *valueFunc) Write(out *dto.Metric) { + populateMetric(v.valType, v.function(), v.labelPairs, out) +} + // NewConstMetric returns a metric with one fixed value that cannot be // changed. Users of this package will not have much use for it in regular // operations. However, when implementing custom Collectors, it is useful as a