diff --git a/metrics/circonus/circonus.go b/metrics/circonus/circonus.go index 68e00ad..7ef50d4 100644 --- a/metrics/circonus/circonus.go +++ b/metrics/circonus/circonus.go @@ -2,6 +2,8 @@ package circonus import ( + "sync" + "github.com/circonus-labs/circonus-gometrics" "github.com/go-kit/kit/metrics" @@ -63,6 +65,8 @@ type Gauge struct { name string m *circonusgometrics.CirconusMetrics + val float64 + mtx sync.RWMutex } // With implements Gauge, but is a no-op, because Circonus metrics have no @@ -70,7 +74,21 @@ func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g } // Set implements Gauge. -func (g *Gauge) Set(value float64) { g.m.SetGauge(g.name, value) } +func (g *Gauge) Set(value float64) { + g.mtx.Lock() + defer g.mtx.Unlock() + g.val = value + g.m.SetGauge(g.name, value) +} + +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { + g.mtx.Lock() + defer g.mtx.Unlock() + value := g.val + delta + g.val = value + g.m.SetGauge(g.name, value) +} // Histogram is a Circonus implementation of a histogram metric. type Histogram struct { diff --git a/metrics/discard/discard.go b/metrics/discard/discard.go index 0bbab8f..a0d3b14 100644 --- a/metrics/discard/discard.go +++ b/metrics/discard/discard.go @@ -25,6 +25,9 @@ // Set implements Gauge. func (g gauge) Set(value float64) {} +// Add implements metrics.Gauge. +func (g gauge) Add(delta float64) {} + type histogram struct{} // NewHistogram returns a new no-op histogram. diff --git a/metrics/dogstatsd/dogstatsd.go b/metrics/dogstatsd/dogstatsd.go index 20a77dc..13e0b4f 100644 --- a/metrics/dogstatsd/dogstatsd.go +++ b/metrics/dogstatsd/dogstatsd.go @@ -72,6 +72,7 @@ return &Gauge{ name: d.prefix + name, obs: d.gauges.Observe, + add: d.gauges.Add, } } @@ -244,6 +245,7 @@ name string lvs lv.LabelValues obs observeFunc + add observeFunc } // With implements metrics.Gauge. @@ -252,12 +254,18 @@ name: g.name, lvs: g.lvs.With(labelValues...), obs: g.obs, + add: g.add, } } // Set implements metrics.Gauge. func (g *Gauge) Set(value float64) { g.obs(g.name, g.lvs, value) +} + +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { + g.add(g.name, g.lvs, delta) } // Timing is a DogStatsD timing, or metrics.Histogram. Observations are diff --git a/metrics/expvar/expvar.go b/metrics/expvar/expvar.go index dcb5d62..ce6f3b8 100644 --- a/metrics/expvar/expvar.go +++ b/metrics/expvar/expvar.go @@ -50,6 +50,9 @@ // Set implements Gauge. func (g *Gauge) Set(value float64) { g.f.Set(value) } +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { g.f.Add(delta) } + // Histogram implements the histogram metric with a combination of the generic // Histogram object and several expvar Floats, one for each of the 50th, 90th, // 95th, and 99th quantiles of observed values, with the quantile attached to diff --git a/metrics/generic/generic.go b/metrics/generic/generic.go index 2f7ae70..295b016 100644 --- a/metrics/generic/generic.go +++ b/metrics/generic/generic.go @@ -105,6 +105,20 @@ atomic.StoreUint64(&g.bits, math.Float64bits(value)) } +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { + for { + var ( + old = atomic.LoadUint64(&g.bits) + newf = math.Float64frombits(old) + delta + new = math.Float64bits(newf) + ) + if atomic.CompareAndSwapUint64(&g.bits, old, new) { + break + } + } +} + // Value returns the current value of the gauge. func (g *Gauge) Value() float64 { return math.Float64frombits(atomic.LoadUint64(&g.bits)) diff --git a/metrics/graphite/graphite.go b/metrics/graphite/graphite.go index 682e147..59532e4 100644 --- a/metrics/graphite/graphite.go +++ b/metrics/graphite/graphite.go @@ -182,6 +182,9 @@ // Set implements gauge. func (g *Gauge) Set(value float64) { g.g.Set(value) } +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { g.g.Add(delta) } + // Histogram is a Graphite histogram metric. Observations are bucketed into // per-quantile gauges. type Histogram struct { diff --git a/metrics/influx/example_test.go b/metrics/influx/example_test.go index 1a5105c..7df3f38 100644 --- a/metrics/influx/example_test.go +++ b/metrics/influx/example_test.go @@ -44,11 +44,14 @@ gauge.With("error", "true").Set(1) gauge.With("error", "false").Set(2) gauge.Set(50) + gauge.With("test", "true").Set(1) + gauge.With("test", "true").Add(1) client := &bufWriter{} in.WriteTo(client) expectedLines := []string{ + `(influx_gauge,a=b,test=true value=2) [0-9]{19}`, `(influx_gauge,a=b value=50) [0-9]{19}`, `(influx_gauge,a=b,error=true value=1) [0-9]{19}`, `(influx_gauge,a=b,error=false value=2) [0-9]{19}`, @@ -59,6 +62,7 @@ } // Output: + // influx_gauge,a=b,test=true value=2 // influx_gauge,a=b value=50 // influx_gauge,a=b,error=true value=1 // influx_gauge,a=b,error=false value=2 diff --git a/metrics/influx/influx.go b/metrics/influx/influx.go index b9a3f66..1ea0cc5 100644 --- a/metrics/influx/influx.go +++ b/metrics/influx/influx.go @@ -66,6 +66,7 @@ return &Gauge{ name: name, obs: in.gauges.Observe, + add: in.gauges.Add, } } @@ -220,6 +221,7 @@ name string lvs lv.LabelValues obs observeFunc + add observeFunc } // With implements metrics.Gauge. @@ -228,12 +230,18 @@ name: g.name, lvs: g.lvs.With(labelValues...), obs: g.obs, + add: g.add, } } // Set implements metrics.Gauge. func (g *Gauge) Set(value float64) { g.obs(g.name, g.lvs, value) +} + +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { + g.add(g.name, g.lvs, delta) } // Histogram is an Influx histrogram. Observations are aggregated into a diff --git a/metrics/internal/lv/space.go b/metrics/internal/lv/space.go index 6807347..672c900 100644 --- a/metrics/internal/lv/space.go +++ b/metrics/internal/lv/space.go @@ -19,6 +19,13 @@ // the vector space, and appends the value to the list of observations. func (s *Space) Observe(name string, lvs LabelValues, value float64) { s.nodeFor(name).observe(lvs, value) +} + +// Add locates the time series identified by the name and label values in +// the vector space, and appends the delta to the last value in the list of +// observations. +func (s *Space) Add(name string, lvs LabelValues, delta float64) { + s.nodeFor(name).add(lvs, delta) } // Walk traverses the vector space and invokes fn for each non-empty time series @@ -91,6 +98,34 @@ child.observe(tail, value) } +func (n *node) add(lvs LabelValues, delta float64) { + n.mtx.Lock() + defer n.mtx.Unlock() + if len(lvs) == 0 { + var value float64 + if len(n.observations) > 0 { + value = last(n.observations) + delta + } else { + value = delta + } + n.observations = append(n.observations, value) + return + } + if len(lvs) < 2 { + panic("too few LabelValues; programmer error!") + } + head, tail := pair{lvs[0], lvs[1]}, lvs[2:] + if n.children == nil { + n.children = map[pair]*node{} + } + child, ok := n.children[head] + if !ok { + child = &node{} + n.children[head] = child + } + child.add(tail, delta) +} + func (n *node) walk(lvs LabelValues, fn func(LabelValues, []float64) bool) bool { n.mtx.RLock() defer n.mtx.RUnlock() @@ -104,3 +139,7 @@ } return true } + +func last(a []float64) float64 { + return a[len(a)-1] +} diff --git a/metrics/metrics.go b/metrics/metrics.go index 719c3d8..a7ba1b1 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -12,6 +12,7 @@ type Gauge interface { With(labelValues ...string) Gauge Set(value float64) + Add(delta float64) } // Histogram describes a metric that takes repeated observations of the same diff --git a/metrics/multi/multi.go b/metrics/multi/multi.go index 971dd17..0f4a9e0 100644 --- a/metrics/multi/multi.go +++ b/metrics/multi/multi.go @@ -54,6 +54,13 @@ return next } +// Add implements metrics.Gauge. +func (g Gauge) Add(delta float64) { + for _, gauge := range g { + gauge.Add(delta) + } +} + // Histogram collects multiple individual histograms and treats them as a unit. type Histogram []metrics.Histogram diff --git a/metrics/multi/multi_test.go b/metrics/multi/multi_test.go index b6f5cfa..bc52f9d 100644 --- a/metrics/multi/multi_test.go +++ b/metrics/multi/multi_test.go @@ -33,8 +33,9 @@ mg.Set(9) mg.Set(8) mg.Set(7) + mg.Add(3) - want := "[9 8 7]" + want := "[9 8 7 10]" for i, m := range []fmt.Stringer{g1, g2, g3} { if have := m.String(); want != have { t.Errorf("g%d: want %q, have %q", i+1, want, have) @@ -76,6 +77,15 @@ func (g *mockGauge) Set(value float64) { g.obs = append(g.obs, value) } func (g *mockGauge) With(...string) metrics.Gauge { return g } func (g *mockGauge) String() string { return fmt.Sprintf("%v", g.obs) } +func (g *mockGauge) Add(delta float64) { + var value float64 + if len(g.obs) > 0 { + value = g.obs[len(g.obs)-1] + delta + } else { + value = delta + } + g.obs = append(g.obs, value) +} type mockHistogram struct { obs []float64 diff --git a/metrics/pcp/pcp.go b/metrics/pcp/pcp.go index a8887a0..e4521c6 100644 --- a/metrics/pcp/pcp.go +++ b/metrics/pcp/pcp.go @@ -82,7 +82,7 @@ func (g *Gauge) Set(value float64) { g.g.Set(value) } // Add adds a value to the gauge. -func (g *Gauge) Add(value float64) { g.g.Inc(value) } +func (g *Gauge) Add(delta float64) { g.g.Inc(delta) } // Histogram wraps a speed Histogram. type Histogram struct { diff --git a/metrics/statsd/statsd.go b/metrics/statsd/statsd.go index 6196105..8dfbf6f 100644 --- a/metrics/statsd/statsd.go +++ b/metrics/statsd/statsd.go @@ -74,6 +74,7 @@ return &Gauge{ name: s.prefix + name, obs: s.gauges.Observe, + add: s.gauges.Add, } } @@ -201,6 +202,7 @@ type Gauge struct { name string obs observeFunc + add observeFunc } // With is a no-op. @@ -211,6 +213,11 @@ // Set implements metrics.Gauge. func (g *Gauge) Set(value float64) { g.obs(g.name, lv.LabelValues{}, value) +} + +// Add implements metrics.Gauge. +func (g *Gauge) Add(delta float64) { + g.add(g.name, lv.LabelValues{}, delta) } // Timing is a StatsD timing, or metrics.Histogram. Observations are diff --git a/metrics/teststat/teststat.go b/metrics/teststat/teststat.go index 991f5c0..5a1885a 100644 --- a/metrics/teststat/teststat.go +++ b/metrics/teststat/teststat.go @@ -42,6 +42,12 @@ f := float64(a[i]) gauge.Set(f) want = f + } + + for i := 0; i < n; i++ { + f := float64(a[i]) + gauge.Add(f) + want += f } if have := value(); want != have {