From 2cf055074f4f55558574e7fb21c7e7901b6ec634 Mon Sep 17 00:00:00 2001 From: "Stephen McQuay (smcquay)" Date: Wed, 13 Dec 2017 15:42:30 -0800 Subject: [PATCH] Adds a FixedPrecision Counter and Gauge --- fixed_precision.go | 151 ++++++++++++++++++++++++++++++++++++++++ fixed_precision_test.go | 133 +++++++++++++++++++++++++++++++++++ interface_test.go | 33 +++++++++ 3 files changed, 317 insertions(+) create mode 100644 fixed_precision.go create mode 100644 fixed_precision_test.go create mode 100644 interface_test.go diff --git a/fixed_precision.go b/fixed_precision.go new file mode 100644 index 0000000..85d4dda --- /dev/null +++ b/fixed_precision.go @@ -0,0 +1,151 @@ +// Package prom exports a collection of prometheus.Metrics that use int64 and +// atomic store and add for maximum speed. +// +// In testing it provides a 10x speedup compared to +// github.com/prometheus/client_golang/prometheus.Metrics. +package prom + +import ( + "math" + "sync/atomic" + "time" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" +) + +// NewCounter returns prometheus.Counter backed by a fixed-precision int64 that +// uses atomic operations. +func NewCounter(opts prometheus.CounterOpts, prec uint) prometheus.Counter { + return NewFixedPrecisionCounter(opts, prec) +} + +// NewGauge returns prometheus.Gauge backed by a fixed-precision int64 that +// uses atomic operations. +func NewGauge(opts prometheus.GaugeOpts, prec uint) prometheus.Gauge { + return NewFixedPrecisionGauge(opts, prec) +} + +// FixedPrecisionGauge implements a prometheus Gauge/Counter metric that uses atomic +// adds and stores for speed. +type FixedPrecisionGauge struct { + val int64 + prec uint + + desc *prometheus.Desc +} + +// NewFixedPrecisionGauge returns a populated fixed-precision counter. +func NewFixedPrecisionGauge(opts prometheus.GaugeOpts, prec uint) *FixedPrecisionGauge { + desc := prometheus.NewDesc( + prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), + opts.Help, + nil, + opts.ConstLabels, + ) + return &FixedPrecisionGauge{ + desc: desc, + prec: uint(math.Pow10(int(prec))), + } +} + +// Set stores the value in the counter. +func (fpg *FixedPrecisionGauge) Set(val float64) { + atomic.StoreInt64(&fpg.val, int64(val)*int64(fpg.prec)) +} + +// add maps delta into the appropriate precision and adds it to val. +func (fpg *FixedPrecisionGauge) add(delta int64) { + atomic.AddInt64(&fpg.val, delta*int64(fpg.prec)) +} + +// Inc adds 1 to the counter. +func (fpg *FixedPrecisionGauge) Inc() { + fpg.add(1) +} + +// Dec decrements 1 from the counter. +func (fpg *FixedPrecisionGauge) Dec() { + fpg.add(-1) +} + +// Add generically adds delta to the value stored by counter. +func (fpg *FixedPrecisionGauge) Add(delta float64) { + atomic.AddInt64(&fpg.val, int64(delta*float64(fpg.prec))) +} + +// Sub is the inverse of Add. +func (fpg *FixedPrecisionGauge) Sub(val float64) { + fpg.Add(val * -1) +} + +// Write is implemented to be useful as a prometheus counter. +func (fpg *FixedPrecisionGauge) Write(out *dto.Metric) error { + f := float64(atomic.LoadInt64(&fpg.val)) / float64(fpg.prec) + out.Counter = &dto.Counter{Value: &f} + return nil +} + +// Value returns a float64 representation of the current value stored. +func (fpg *FixedPrecisionGauge) Value() float64 { + return float64(atomic.LoadInt64(&fpg.val)) / float64(fpg.prec) +} + +// The following three methods exist to make this behave with Prometheus + +// Desc returns this FixedPrecisionGauge's prometheus description. +func (fpg *FixedPrecisionGauge) Desc() *prometheus.Desc { + return fpg.desc +} + +// Describe sends the counter's description to the chan +func (fpg *FixedPrecisionGauge) Describe(dc chan<- *prometheus.Desc) { + dc <- fpg.desc +} + +// Collect sends the counter value to the chan +func (fpg *FixedPrecisionGauge) Collect(mc chan<- prometheus.Metric) { + mc <- fpg +} + +// SetToCurrentTime sets the Gauge to the current Unix time in seconds. +// +// Beware that if precision is set too high (greater than 9) it can overflow +// the underlying int64. +func (fpg *FixedPrecisionGauge) SetToCurrentTime() { + fpg.Set(float64(time.Now().Unix())) +} + +// FixedPrecisionCounter embeds FixedPrecisionGauge and enforces the same +// guarantees as a prometheus.Counter where negative adds panic. +type FixedPrecisionCounter struct { + FixedPrecisionGauge +} + +// NewFixedPrecisionCounter creates a FixedPrecisionCounter based on the +// provided Opts. It matches prometheus.Counter behavior by panicking if adding +// negative numbers. +func NewFixedPrecisionCounter(opts prometheus.CounterOpts, prec uint) *FixedPrecisionCounter { + desc := prometheus.NewDesc( + prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), + opts.Help, + nil, + opts.ConstLabels, + ) + return &FixedPrecisionCounter{ + FixedPrecisionGauge: FixedPrecisionGauge{ + desc: desc, + prec: uint(math.Pow10(int(prec))), + }, + } + +} + +// Add adds the given value to the counter. It matches prometheus.Counter +// behavior by panicking if the value is < 0. +func (fpc *FixedPrecisionCounter) Add(v float64) { + if v < 0 { + panic("counter cannot decrease in value") + } + fpc.FixedPrecisionGauge.Add(v) +} diff --git a/fixed_precision_test.go b/fixed_precision_test.go new file mode 100644 index 0000000..b6cf292 --- /dev/null +++ b/fixed_precision_test.go @@ -0,0 +1,133 @@ +package prom + +import ( + "math" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" +) + +func TestNoPrecisionGauge(t *testing.T) { + c := NewFixedPrecisionGauge(prometheus.GaugeOpts{ + Name: "test", + Help: "test help", + }, 0) + + c.Inc() + var want float64 = 1 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %v, got %v.", expected, got) + } + c.Add(0.9999999999999999999) + want = 2 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %v, got %v.", expected, got) + } + + c.Sub(0.9999999999999999999 * 3) + want = -1 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %v, got %v.", expected, got) + } + + m := &dto.Metric{} + c.Write(m) + + if expected, got := `counter: `, m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} + +func TestFixedPrecisionAdd(t *testing.T) { + c := NewFixedPrecisionGauge(prometheus.GaugeOpts{ + Name: "test", + Help: "test help", + }, 3) + + c.Inc() + want := 1.0 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + c.Add(42.3) + want = 43.3 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + + m := &dto.Metric{} + c.Write(m) + + if expected, got := `counter: `, m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} + +func TestFixedPrecisionSub(t *testing.T) { + c := NewFixedPrecisionGauge(prometheus.GaugeOpts{ + Name: "test", + Help: "test help", + }, 3) + + c.Dec() + var want float64 = -1 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + c.Sub(42.3) + want = -43.3 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + + m := &dto.Metric{} + c.Write(m) + + if expected, got := `counter: `, m.String(); expected != got { + t.Errorf("expected %q, got %q", expected, got) + } +} + +func TestSetToCurrentTime(t *testing.T) { + precs := []uint{} + var i uint + for i = 0; i < 10; i++ { + precs = append(precs, i) + } + + for _, prec := range precs { + c := NewFixedPrecisionGauge(prometheus.GaugeOpts{ + Name: "test", + Help: "test help", + }, prec) + + c.SetToCurrentTime() + n := time.Now() + + delta := math.Abs(c.Value() - float64(n.Unix())) + if !(delta <= 1) { + t.Fatalf("SetToCurrentTime at %v precision was off from time.Now(): got: %v, want: <= 1", prec, delta) + } + } +} + +func TestCounterDirection(t *testing.T) { + defer func() { + if e := recover(); e == nil { + t.Fatalf("did not panic and shold have") + } + }() + + c := NewFixedPrecisionCounter(prometheus.CounterOpts{ + Name: "test", + Help: "test help", + }, 3) + c.Add(1) + var want float64 = 1 + if expected, got := want, c.Value(); expected != got { + t.Errorf("Expected %f, got %f.", expected, got) + } + c.Add(-1) +} diff --git a/interface_test.go b/interface_test.go new file mode 100644 index 0000000..2dfcb1f --- /dev/null +++ b/interface_test.go @@ -0,0 +1,33 @@ +package prom + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestGauginess(t *testing.T) { + g := NewGauge(prometheus.GaugeOpts{ + Name: "test", + Help: "test help", + }, 3) + + switch g.(type) { + case prometheus.Gauge: + default: + t.Fatalf("FixedPrecision is not a prometheus.Gauge") + } +} + +func TestCounteriness(t *testing.T) { + c := NewCounter(prometheus.CounterOpts{ + Name: "test", + Help: "test help", + }, 3) + + switch c.(type) { + case prometheus.Counter: + default: + t.Fatalf("FixedPrecision is not a prometheus.Counter") + } +}