Skip to content

Timer helpers #1854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 317 additions & 0 deletions prometheus/timer.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,320 @@ func (t *Timer) ObserveDurationWithExemplar(exemplar Labels) time.Duration {
}
return d
}

// TimerHistogram is a thin convenience wrapper around a Prometheus Histogram
// that makes it easier to time code paths in an idiomatic Go-style:
//
// defer timer.Observe()() // <-- starts the timer and defers the stop
type TimerHistogram struct {
Histogram
}

func NewTimerHistogram(opts HistogramOpts) *TimerHistogram {
t := &TimerHistogram{
Histogram: NewHistogram(opts),
}
return t
}

// Observe starts a prom.Timer that records into the embedded Histogram and
// returns a “stop” callback. Best used with defer:
//
// defer timer.Observe()()
//
// The inner closure calls ObserveDuration on the hidden prom.Timer, recording
// the elapsed seconds into the histogram’s current bucket.
func (t *TimerHistogram) Observe() func() {
timer := NewTimer(t.Histogram)
return func() {
timer.ObserveDuration()
}
}

// Wrap executes fn() and records the time it took. Equivalent to:
// Use when you don't need a defer chain (e.g., inside small helpers).
func (t *TimerHistogram) Wrap(fn func()) {
defer t.Observe()()
fn()
}

type TimerHistogramVec struct {
*HistogramVec
}

func NewTimerHistogramVec(opts HistogramOpts, labelNames []string) *TimerHistogramVec {
t := &TimerHistogramVec{
HistogramVec: NewHistogramVec(opts, labelNames),
}

return t
}

// Observe return func for stop timer and observe value
// Example
// defer metric.Observe(map[string]string{"foo": "bar"})()
func (t *TimerHistogramVec) Observe(labels map[string]string) func() {
timeStart := time.Now()
return func() {
d := time.Since(timeStart)
t.HistogramVec.With(labels).Observe(d.Seconds())
}
}

func (t *TimerHistogramVec) ObserveLabelValues(values ...string) func() {
timeStart := time.Now()
return func() {
d := time.Since(timeStart)
t.HistogramVec.WithLabelValues(values...).Observe(d.Seconds())
}
}

func (t *TimerHistogramVec) Wrap(labels map[string]string, fn func()) {
defer t.Observe(labels)()
fn()
}

func (t *TimerHistogramVec) WrapLabelValues(values []string, fn func()) {
defer t.ObserveLabelValues(values...)()
fn()
}

// TimerCounter is a minimal helper that turns a Prometheus **Counter** into a
// “stop-watch” for wall-clock time.
//
// Each call to Observe() starts a timer and, when the returned closure is
// executed, adds the elapsed seconds to the embedded Counter. The counter
// therefore represents **the cumulative running time** across many code paths
// (e.g. total time spent processing all requests since process start).
//
// Compared with a Histogram-based timer you gain:
//
// - A single monotonically-increasing number that is cheap to aggregate or
// alert on (e.g. “CPU-seconds spent in GC”).
// - Zero bucket management or percentile math.
//
// But you lose per-request latency data, so use it when you care about total
// time rather than distribution.
type TimerCounter struct {
Counter
}

func NewTimerCounter(opts Opts) *TimerCounter {
t := &TimerCounter{
Counter: NewCounter(CounterOpts(opts)),
}

return t
}

// Observe starts a wall-clock timer and returns a “stop” closure.
//
// Typical usage:
//
// defer myCounter.Observe()() // records on function exit
//
// When the closure is executed it records the elapsed duration (in seconds)
// into the Counter. Thread-safe as long as the underlying Counter is
// thread-safe (Prometheus counters are).
func (t *TimerCounter) Observe() func() {
start := time.Now()

return func() {
d := time.Since(start)
t.Add(d.Seconds())
}
}

func (t *TimerCounter) Wrap(fn func()) {
defer t.Observe()()
fn()
}

type TimerCounterVec struct {
*CounterVec
}

func NewTimerCounterVec(opts Opts, labels []string) *TimerCounterVec {
t := &TimerCounterVec{
CounterVec: NewCounterVec(CounterOpts(opts), labels),
}

return t
}

func (t *TimerCounterVec) Observe(labels map[string]string) func() {
start := time.Now()

return func() {
d := time.Since(start)
t.With(labels).Add(d.Seconds())
}
}

func (t *TimerCounterVec) ObserveLabelValues(values ...string) func() {
start := time.Now()
return func() {
d := time.Since(start)
t.WithLabelValues(values...).Add(d.Seconds())
}
}

func (t *TimerCounterVec) Wrap(labels map[string]string, fn func()) {
defer t.Observe(labels)()
fn()
}

func (t *TimerCounterVec) WrapLabelValues(values []string, fn func()) {
defer t.ObserveLabelValues(values...)()
fn()
}

// TimerContinuous is a variant of the standard Timer that **continuously updates**
// its underlying Counter while it is running—by default once every second—
// instead of emitting a single measurement only when the timer stops.
//
// Trade-offs
// ----------
// - **Higher overhead** than a one-shot Timer (extra goroutine + ticker).
// - **Finer-grained metrics** that are invaluable for long-running or
// indeterminate-length activities such as stream processing, background
// jobs, or large file transfers.
// - **Sensitive to clock skew**—if the system clock is moved **backwards**
// while the timer is running, the negative delta is silently discarded
// (no panic), so that slice of time is lost from the measurement.
type TimerContinuous struct {
Counter
updateInterval time.Duration
}

func NewTimerContinuous(opts Opts, updateInterval time.Duration) *TimerContinuous {
t := &TimerContinuous{
Counter: NewCounter(CounterOpts(opts)),
updateInterval: updateInterval,
}
if t.updateInterval == 0 {
t.updateInterval = time.Second
}

return t
}

func (t *TimerContinuous) Observe() func() {
start := time.Now()
ch := make(chan struct{})

go func() {
added := float64(0)
ticker := time.NewTicker(t.updateInterval)
defer ticker.Stop()
for {
select {
case <-ch:
d := time.Since(start)
if diff := d.Seconds() - added; diff > 0 {
t.Add(diff)
}
return
case <-ticker.C:
d := time.Since(start)
if diff := d.Seconds() - added; diff > 0 {
t.Add(diff)
added += diff
}
}
}
}()

return func() {
ch <- struct{}{}
}
}

func (t *TimerContinuous) Wrap(fn func()) {
defer t.Observe()()
fn()
}

type TimerContinuousVec struct {
*CounterVec
}

func NewTimerContinuousVec(opts Opts, labels []string) *TimerCounterVec {
t := &TimerCounterVec{
CounterVec: NewCounterVec(CounterOpts(opts), labels),
}

return t
}

func (t *TimerContinuousVec) Observe(labels map[string]string) func() {
start := time.Now()
ch := make(chan struct{})

go func() {
added := float64(0)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ch:
d := time.Since(start)
if diff := d.Seconds() - added; diff > 0 {
t.With(labels).Add(diff)
}
return
case <-ticker.C:
d := time.Since(start)
if diff := d.Seconds() - added; diff > 0 {
t.With(labels).Add(diff)
added += diff
}
}
}
}()

return func() {
ch <- struct{}{}
}
}

func (t *TimerContinuousVec) ObserveLabelValues(values ...string) func() {
start := time.Now()
ch := make(chan struct{})

go func() {
added := float64(0)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ch:
d := time.Since(start)
if diff := d.Seconds() - added; diff > 0 {
t.WithLabelValues(values...).Add(diff)
}
return
case <-ticker.C:
d := time.Since(start)
if diff := d.Seconds() - added; diff > 0 {
t.WithLabelValues(values...).Add(diff)
added += diff
}
}
}
}()

return func() {
ch <- struct{}{}
}
}

func (t *TimerContinuousVec) Wrap(labels map[string]string, fn func()) {
defer t.Observe(labels)()
fn()
}

func (t *TimerContinuousVec) WrapLabelValues(values []string, fn func()) {
defer t.ObserveLabelValues(values...)()
fn()
}
Loading