Change the Timer API

This finally makes the (presumably) common simple case as simple as it
gets.

The still quite common (but less common) case of using a Gauge is
slightly more verbose now, but not needing to provide a separate
constructor is totally worth it.

Finally, the advanced use case is not really more verbose as in my
original suggestion. However, the logic to decide which Observer to
use is now all in the ObserverFunc handed in at construction
time. This is deliberate and desired. It makes sure the selection
mechanism is all spelled out there. No surprises buried deep in the
function code somewhere.
This commit is contained in:
beorn7 2016-11-16 18:17:50 +01:00
parent 18c13ef63d
commit 89ca0458cb
5 changed files with 279 additions and 102 deletions

View File

@ -0,0 +1,48 @@
// Copyright 2014 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus_test
import (
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
// For periodic or batch processes that run much less frequently than
// the Prometheus scrape, it makes sense to use a Gauge to observe the
// duration.
// See https://prometheus.io/docs/practices/instrumentation/#batch-jobs
batchDuration = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "example_batch_duration_seconds",
Help: "Duration of the last batch run.",
})
)
func performBatch() {
// The Set method of the Gauge is used to observe the duration.
timer := prometheus.NewTimer(prometheus.ObserverFunc(batchDuration.Set))
defer timer.ObserveDuration()
// Actually perform the batch of work.
// ...
}
func ExampleTimer_batch() {
// 10m is much longer than the usual scrape interval of Prometheus.
c := time.Tick(10 * time.Minute)
for _ = range c {
performBatch()
}
}

View File

@ -0,0 +1,69 @@
// Copyright 2014 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus_test
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
)
var (
// apiRequestDuration tracks the duration separate for each HTTP status
// class (1xx, 2xx, ...). This creates a fair amount of time series on
// the Prometheus server. Usually, you would track the duration of
// serving HTTP request without partitioning by outcome. Do something
// like this only if needed. Also note how only status classes are
// tracked, not every single status code. The latter would create an
// even larger amount of time series. Request counters partitioned by
// status code are usually OK as each counter only creates one time
// series. Histograms are way more expensive, so partition with care and
// only where you really need separate latency tracking.
apiRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "api_request_duration_seconds",
Help: "Histogram for the request duration of the public API, partitioned by status class.",
Buckets: prometheus.ExponentialBuckets(0.1, 1.5, 5),
},
[]string{"status_class"},
)
)
func handler(w http.ResponseWriter, r *http.Request) {
status := http.StatusOK
// The ObserverFunc gets called by the deferred ObserveDuration and
// decides wich Histogram's Observe method is called.
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
switch {
case status >= http.StatusInternalServerError:
apiRequestDuration.WithLabelValues("5xx").Observe(v)
case status >= http.StatusBadRequest:
apiRequestDuration.WithLabelValues("4xx").Observe(v)
case status >= http.StatusMultipleChoices:
apiRequestDuration.WithLabelValues("3xx").Observe(v)
case status >= http.StatusOK:
apiRequestDuration.WithLabelValues("2xx").Observe(v)
default:
apiRequestDuration.WithLabelValues("1xx").Observe(v)
}
}))
defer timer.ObserveDuration()
// Handle the request. Set status accordingly.
// ...
}
func ExampleTimer_complex() {
http.HandleFunc("/api", handler)
}

View File

@ -0,0 +1,40 @@
// Copyright 2014 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prometheus_test
import (
"math/rand"
"time"
"github.com/prometheus/client_golang/prometheus"
)
var (
requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "example_request_duration_seconds",
Help: "Histogram for the runtime of a simple example function.",
Buckets: prometheus.LinearBuckets(0.01, 0.01, 10),
})
)
func ExampleTimer() {
// timer times this example function. It uses a Histogram, but a Summary
// would also work, as both implement Observer. Check out
// https://prometheus.io/docs/practices/histograms/ for differences.
timer := prometheus.NewTimer(requestDuration)
defer timer.ObserveDuration()
// Do something here that takes time.
time.Sleep(time.Duration(rand.NormFloat64()*10000+50000) * time.Microsecond)
}

View File

@ -15,83 +15,58 @@ package prometheus
import "time" import "time"
// Observer is the interface that wraps the Observe method, used by Histogram // Observer is the interface that wraps the Observe method, which is used by
// and Summary to add observations. // Histogram and Summary to add observations.
type Observer interface { type Observer interface {
Observe(float64) Observe(float64)
} }
// observerFunc is a function that implements Observer. // The ObserverFunc type is an adapter to allow the use of ordinary
type observerFunc func(float64) // functions as Observers. If f is a function with the appropriate
// signature, ObserverFunc(f) is an Observer that calls f.
//
// This adapter is usually used in connection with the Timer type, and there are
// two general use cases:
//
// The most common one is to use a Gauge as the Observer for a Timer.
// See the "Batch" Timer example.
//
// The more advanced use case is to create a function that dynamically decides
// which Observer to use for observing the duration. See the "Complex" Timer
// example.
type ObserverFunc func(float64)
func (o observerFunc) Observe(value float64) { // Observe calls f(value). It implements Observer.
o(value) func (f ObserverFunc) Observe(value float64) {
f(value)
} }
// Timer is a helper type to time functions. Use NewTimer to create new // Timer is a helper type to time functions. Use NewTimer to create new
// instances. The most common usage pattern is to time a function with a // instances.
// Histogram in the following way:
// func timeMe() {
// t := NewTimer().With(myHistogram)
// defer t.ObserveDuration()
// // Do something.
// }
// See the documentation of the methods for special cases.
type Timer struct { type Timer struct {
begin time.Time begin time.Time
observer Observer observer Observer
} }
// NewTimer creates a new Timer. To time anything, an Observer (i.e. a Histogram // NewTimer creates a new Timer. The provided Observer is used to observe a
// or a Summary) or a Gague has to be bound to it, the former with the With // duration in seconds. Timer is usually used to time a function call in the
// method, the latter with the WithGauge method. // following way:
func NewTimer() *Timer { // func TimeMe() {
return &Timer{begin: time.Now()} // timer := NewTimer(myHistogram)
// defer timer.ObserveDuration()
// // Do actual work.
// }
func NewTimer(o Observer) *Timer {
return &Timer{
begin: time.Now(),
observer: o,
}
} }
// With binds an Observer (i.e. a Histogram or a Summary) to the Timer and // ObserveDuration records the duration passed since the Timer was created with
// returns the Timer for convenience. In most cases, the Observer to be used is // NewTimer. It calls the Observe method of the Observer provided during
// already known at construction time, so it can be set immediately: // construction with the duration in seconds as an argument. ObserveDuration is
// t := NewTimer().With(myObserver) // usually called with a defer statement.
//
// With can also be called later, to bind an Observer for the first time or to
// change the bound Observer. It can be called with nil to unbind a previously
// bound Observer. Both is useful for recording with different Observers (or
// none) depending on the outcome of the timed function. Note that timing
// depending on the outcome can be confusing and should only be used after
// careful consideration.
//
// The ObserveDuration method of the Timer records the duration by calling the
// Observe method of the bound Observer with the time elapsed since NewTimer was
// called (in seconds).
//
// Note that a Gauge bound with WithGauge overrides the Observer bound with With
// and vice versa.
func (t *Timer) With(o Observer) *Timer {
t.observer = o
return t
}
// WithGauge binds a Gauge to the Timer and returns the Timer for
// convenience. This works in the same way as With works for Observers. To
// record the time, the Set method of the Gauge is called (with the recorded
// duration in seconds). Note that timing with Gauges is only useful for events
// that happen less frequently than the scrape interval or for one-shot batch
// jobs (where the recorded duration is pushed to a Pushgateway).
//
// Note that a Gauge bound with WithGauge overrides the Observer bound with With
// and vice versa.
func (t *Timer) WithGauge(g Gauge) *Timer {
t.observer = observerFunc(g.Set)
return t
}
// ObserveDuration records the duration passed since NewTimer was called. If an
// Observer has been bound with the With method, ObserveDuration calls its
// Observe method with the duration in seconds as an argument. If a Gauge has
// been bound with the WithGauge method, ObserveDuration calls its Set method
// with the duration in seconds as an argument. ObserveDuration is usually
// called with a defer statement.
func (t *Timer) ObserveDuration() { func (t *Timer) ObserveDuration() {
if t.observer != nil { if t.observer != nil {
t.observer.Observe(time.Since(t.begin).Seconds()) t.observer.Observe(time.Since(t.begin).Seconds())

View File

@ -20,20 +20,16 @@ import (
) )
func TestTimerObserve(t *testing.T) { func TestTimerObserve(t *testing.T) {
his := NewHistogram(HistogramOpts{ var (
Name: "test_histogram", his = NewHistogram(HistogramOpts{Name: "test_histogram"})
}) sum = NewSummary(SummaryOpts{Name: "test_summary"})
sum := NewSummary(SummaryOpts{ gauge = NewGauge(GaugeOpts{Name: "test_gauge"})
Name: "test_summary", )
})
gauge := NewGauge(GaugeOpts{
Name: "test_gauge",
})
func() { func() {
hisTimer := NewTimer().With(his) hisTimer := NewTimer(his)
sumTimer := NewTimer().With(sum) sumTimer := NewTimer(sum)
gaugeTimer := NewTimer().WithGauge(gauge) gaugeTimer := NewTimer(ObserverFunc(gauge.Set))
defer hisTimer.ObserveDuration() defer hisTimer.ObserveDuration()
defer sumTimer.ObserveDuration() defer sumTimer.ObserveDuration()
defer gaugeTimer.ObserveDuration() defer gaugeTimer.ObserveDuration()
@ -57,51 +53,100 @@ func TestTimerObserve(t *testing.T) {
} }
func TestTimerEmpty(t *testing.T) { func TestTimerEmpty(t *testing.T) {
emptyTimer := NewTimer() emptyTimer := NewTimer(nil)
emptyTimer.ObserveDuration() emptyTimer.ObserveDuration()
// Do nothing, just demonstrate it works without panic. // Do nothing, just demonstrate it works without panic.
} }
func TestTimerUnset(t *testing.T) { func TestTimerConditionalTiming(t *testing.T) {
his := NewHistogram(HistogramOpts{ var (
Name: "test_histogram", his = NewHistogram(HistogramOpts{
}) Name: "test_histogram",
})
timeMe = true
m = &dto.Metric{}
)
func() { timedFunc := func() {
timer := NewTimer().With(his) timer := NewTimer(ObserverFunc(func(v float64) {
if timeMe {
his.Observe(v)
}
}))
defer timer.ObserveDuration() defer timer.ObserveDuration()
timer.With(nil) }
}()
m := &dto.Metric{} timedFunc() // This will time.
his.Write(m) his.Write(m)
if want, got := uint64(0), m.GetHistogram().GetSampleCount(); want != got { if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for histogram, got %d", want, got)
}
timeMe = false
timedFunc() // This will not time again.
m.Reset()
his.Write(m)
if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for histogram, got %d", want, got) t.Errorf("want %d observations for histogram, got %d", want, got)
} }
} }
func TestTimerChange(t *testing.T) { func TestTimerByOutcome(t *testing.T) {
his := NewHistogram(HistogramOpts{ var (
Name: "test_histogram", his = NewHistogramVec(
}) HistogramOpts{Name: "test_histogram"},
sum := NewSummary(SummaryOpts{ []string{"outcome"},
Name: "test_summary", )
}) outcome = "foo"
m = &dto.Metric{}
)
func() { timedFunc := func() {
timer := NewTimer().With(his) timer := NewTimer(ObserverFunc(func(v float64) {
his.WithLabelValues(outcome).Observe(v)
}))
defer timer.ObserveDuration() defer timer.ObserveDuration()
timer.With(sum)
}()
m := &dto.Metric{} if outcome == "foo" {
his.Write(m) outcome = "bar"
return
}
outcome = "foo"
}
timedFunc()
his.WithLabelValues("foo").Write(m)
if want, got := uint64(0), m.GetHistogram().GetSampleCount(); want != got { if want, got := uint64(0), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for histogram, got %d", want, got) t.Errorf("want %d observations for 'foo' histogram, got %d", want, got)
} }
m.Reset() m.Reset()
sum.Write(m) his.WithLabelValues("bar").Write(m)
if want, got := uint64(1), m.GetSummary().GetSampleCount(); want != got { if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for summary, got %d", want, got) t.Errorf("want %d observations for 'bar' histogram, got %d", want, got)
} }
timedFunc()
m.Reset()
his.WithLabelValues("foo").Write(m)
if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for 'foo' histogram, got %d", want, got)
}
m.Reset()
his.WithLabelValues("bar").Write(m)
if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for 'bar' histogram, got %d", want, got)
}
timedFunc()
m.Reset()
his.WithLabelValues("foo").Write(m)
if want, got := uint64(1), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for 'foo' histogram, got %d", want, got)
}
m.Reset()
his.WithLabelValues("bar").Write(m)
if want, got := uint64(2), m.GetHistogram().GetSampleCount(); want != got {
t.Errorf("want %d observations for 'bar' histogram, got %d", want, got)
}
} }