1
0
mirror of https://github.com/go-kit/kit.git synced 2025-07-17 01:12:38 +02:00

Add support for metrics.Histogram distribution

Histograms gain a method to read the distribution (slice of buckets)
that has been observed so far. In this PR, the only implementation that
supports Distribution is expvar (via codahale/hdrhistogram). Prometheus
support is possible and planned.

- Each metrics type gains a Name() method
- metrics.Histogram gains Distribution()
- metrics package gains PrintDistribution()
- Minor updates to README
This commit is contained in:
Peter Bourgon
2016-02-04 20:54:08 +01:00
parent e553b8eff9
commit 49d907564d
10 changed files with 262 additions and 66 deletions

View File

@ -13,7 +13,11 @@ It has **[counters][]**, **[gauges][]**, and **[histograms][]**,
## Rationale ## Rationale
TODO Code instrumentation is absolutely essential to achieve [observability][] into a distributed system.
Metrics and instrumentation tools have coalesced around a few well-defined idioms.
`package metrics` provides a common, minimal interface those idioms for service authors.
[observability]: https://speakerdeck.com/mattheath/observability-in-micro-service-architectures
## Usage ## Usage
@ -53,6 +57,7 @@ func handleRequest() {
``` ```
A gauge for the number of goroutines currently running, exported via statsd. A gauge for the number of goroutines currently running, exported via statsd.
```go ```go
import ( import (
"net" "net"
@ -66,14 +71,13 @@ import (
func main() { func main() {
statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") statsdWriter, err := net.Dial("udp", "127.0.0.1:8126")
if err != nil { if err != nil {
os.Exit(1) panic(err)
} }
reportingDuration := 5 * time.Second reportInterval := 5 * time.Second
goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportingDuration) goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportInterval)
for range time.Tick(reportingDuration) { for range time.Tick(reportInterval) {
goroutines.Set(float64(runtime.NumGoroutine())) goroutines.Set(float64(runtime.NumGoroutine()))
} }
} }
``` ```

View File

@ -29,34 +29,42 @@ import (
) )
type counter struct { type counter struct {
v *expvar.Int name string
v *expvar.Int
} }
// NewCounter returns a new Counter backed by an expvar with the given name. // NewCounter returns a new Counter backed by an expvar with the given name.
// Fields are ignored. // Fields are ignored.
func NewCounter(name string) metrics.Counter { func NewCounter(name string) metrics.Counter {
return &counter{expvar.NewInt(name)} return &counter{
name: name,
v: expvar.NewInt(name),
}
} }
func (c *counter) Name() string { return c.name }
func (c *counter) With(metrics.Field) metrics.Counter { return c } func (c *counter) With(metrics.Field) metrics.Counter { return c }
func (c *counter) Add(delta uint64) { c.v.Add(int64(delta)) } func (c *counter) Add(delta uint64) { c.v.Add(int64(delta)) }
type gauge struct { type gauge struct {
v *expvar.Float name string
v *expvar.Float
} }
// NewGauge returns a new Gauge backed by an expvar with the given name. It // NewGauge returns a new Gauge backed by an expvar with the given name. It
// should be updated manually; for a callback-based approach, see // should be updated manually; for a callback-based approach, see
// PublishCallbackGauge. Fields are ignored. // PublishCallbackGauge. Fields are ignored.
func NewGauge(name string) metrics.Gauge { func NewGauge(name string) metrics.Gauge {
return &gauge{expvar.NewFloat(name)} return &gauge{
name: name,
v: expvar.NewFloat(name),
}
} }
func (g *gauge) Name() string { return g.name }
func (g *gauge) With(metrics.Field) metrics.Gauge { return g } func (g *gauge) With(metrics.Field) metrics.Gauge { return g }
func (g *gauge) Add(delta float64) { g.v.Add(delta) }
func (g *gauge) Add(delta float64) { g.v.Add(delta) } func (g *gauge) Set(value float64) { g.v.Set(value) }
func (g *gauge) Set(value float64) { g.v.Set(value) }
// PublishCallbackGauge publishes a Gauge as an expvar with the given name, // PublishCallbackGauge publishes a Gauge as an expvar with the given name,
// whose value is determined at collect time by the passed callback function. // whose value is determined at collect time by the passed callback function.
@ -101,6 +109,7 @@ func NewHistogram(name string, minValue, maxValue int64, sigfigs int, quantiles
return h return h
} }
func (h *histogram) Name() string { return h.name }
func (h *histogram) With(metrics.Field) metrics.Histogram { return h } func (h *histogram) With(metrics.Field) metrics.Histogram { return h }
func (h *histogram) Observe(value int64) { func (h *histogram) Observe(value int64) {
@ -117,6 +126,19 @@ func (h *histogram) Observe(value int64) {
} }
} }
func (h *histogram) Distribution() []metrics.Bucket {
bars := h.hist.Current.Distribution()
buckets := make([]metrics.Bucket, len(bars))
for i, bar := range bars {
buckets[i] = metrics.Bucket{
From: bar.From,
To: bar.To,
Count: bar.Count,
}
}
return buckets
}
func (h *histogram) rotateLoop(d time.Duration) { func (h *histogram) rotateLoop(d time.Duration) {
for range time.Tick(d) { for range time.Tick(d) {
h.mu.Lock() h.mu.Lock()

View File

@ -12,7 +12,7 @@ import (
func TestHistogramQuantiles(t *testing.T) { func TestHistogramQuantiles(t *testing.T) {
var ( var (
name = "test_histogram" name = "test_histogram_quantiles"
quantiles = []int{50, 90, 95, 99} quantiles = []int{50, 90, 95, 99}
h = expvar.NewHistogram(name, 0, 100, 3, quantiles...).With(metrics.Field{Key: "ignored", Value: "field"}) h = expvar.NewHistogram(name, 0, 100, 3, quantiles...).With(metrics.Field{Key: "ignored", Value: "field"})
) )

View File

@ -9,6 +9,7 @@ package metrics
// between measurements of a counter over intervals of time, an aggregation // between measurements of a counter over intervals of time, an aggregation
// layer can derive rates, acceleration, etc. // layer can derive rates, acceleration, etc.
type Counter interface { type Counter interface {
Name() string
With(Field) Counter With(Field) Counter
Add(delta uint64) Add(delta uint64)
} }
@ -16,6 +17,7 @@ type Counter interface {
// Gauge captures instantaneous measurements of something using signed, 64-bit // Gauge captures instantaneous measurements of something using signed, 64-bit
// floats. The value does not need to be monotonic. // floats. The value does not need to be monotonic.
type Gauge interface { type Gauge interface {
Name() string
With(Field) Gauge With(Field) Gauge
Set(value float64) Set(value float64)
Add(delta float64) Add(delta float64)
@ -25,8 +27,10 @@ type Gauge interface {
// milliseconds it takes to handle requests). Implementations may choose to // milliseconds it takes to handle requests). Implementations may choose to
// add gauges for values at meaningful quantiles. // add gauges for values at meaningful quantiles.
type Histogram interface { type Histogram interface {
Name() string
With(Field) Histogram With(Field) Histogram
Observe(value int64) Observe(value int64)
Distribution() []Bucket
} }
// Field is a key/value pair associated with an observation for a specific // Field is a key/value pair associated with an observation for a specific
@ -35,3 +39,10 @@ type Field struct {
Key string Key string
Value string Value string
} }
// Bucket is a range in a histogram which aggregates observations.
type Bucket struct {
From int64
To int64
Count int64
}

View File

@ -1,73 +1,107 @@
package metrics package metrics
type multiCounter []Counter type multiCounter struct {
name string
// NewMultiCounter returns a wrapper around multiple Counters. a []Counter
func NewMultiCounter(counters ...Counter) Counter {
c := make(multiCounter, 0, len(counters))
return append(c, counters...)
} }
// NewMultiCounter returns a wrapper around multiple Counters.
func NewMultiCounter(name string, counters ...Counter) Counter {
return &multiCounter{
name: name,
a: counters,
}
}
func (c multiCounter) Name() string { return c.name }
func (c multiCounter) With(f Field) Counter { func (c multiCounter) With(f Field) Counter {
next := make(multiCounter, len(c)) next := &multiCounter{
for i, counter := range c { name: c.name,
next[i] = counter.With(f) a: make([]Counter, len(c.a)),
}
for i, counter := range c.a {
next.a[i] = counter.With(f)
} }
return next return next
} }
func (c multiCounter) Add(delta uint64) { func (c multiCounter) Add(delta uint64) {
for _, counter := range c { for _, counter := range c.a {
counter.Add(delta) counter.Add(delta)
} }
} }
type multiGauge []Gauge type multiGauge struct {
name string
a []Gauge
}
func (g multiGauge) Name() string { return g.name }
// NewMultiGauge returns a wrapper around multiple Gauges. // NewMultiGauge returns a wrapper around multiple Gauges.
func NewMultiGauge(gauges ...Gauge) Gauge { func NewMultiGauge(name string, gauges ...Gauge) Gauge {
g := make(multiGauge, 0, len(gauges)) return &multiGauge{
return append(g, gauges...) name: name,
a: gauges,
}
} }
func (g multiGauge) With(f Field) Gauge { func (g multiGauge) With(f Field) Gauge {
next := make(multiGauge, len(g)) next := &multiGauge{
for i, gauge := range g { name: g.name,
next[i] = gauge.With(f) a: make([]Gauge, len(g.a)),
}
for i, gauge := range g.a {
next.a[i] = gauge.With(f)
} }
return next return next
} }
func (g multiGauge) Set(value float64) { func (g multiGauge) Set(value float64) {
for _, gauge := range g { for _, gauge := range g.a {
gauge.Set(value) gauge.Set(value)
} }
} }
func (g multiGauge) Add(delta float64) { func (g multiGauge) Add(delta float64) {
for _, gauge := range g { for _, gauge := range g.a {
gauge.Add(delta) gauge.Add(delta)
} }
} }
type multiHistogram []Histogram type multiHistogram struct {
name string
// NewMultiHistogram returns a wrapper around multiple Histograms. a []Histogram
func NewMultiHistogram(histograms ...Histogram) Histogram {
h := make(multiHistogram, 0, len(histograms))
return append(h, histograms...)
} }
// NewMultiHistogram returns a wrapper around multiple Histograms.
func NewMultiHistogram(name string, histograms ...Histogram) Histogram {
return &multiHistogram{
name: name,
a: histograms,
}
}
func (h multiHistogram) Name() string { return h.name }
func (h multiHistogram) With(f Field) Histogram { func (h multiHistogram) With(f Field) Histogram {
next := make(multiHistogram, len(h)) next := &multiHistogram{
for i, histogram := range h { name: h.name,
next[i] = histogram.With(f) a: make([]Histogram, len(h.a)),
}
for i, histogram := range h.a {
next.a[i] = histogram.With(f)
} }
return next return next
} }
func (h multiHistogram) Observe(value int64) { func (h multiHistogram) Observe(value int64) {
for _, histogram := range h { for _, histogram := range h.a {
histogram.Observe(value) histogram.Observe(value)
} }
} }
func (h multiHistogram) Distribution() []Bucket {
return []Bucket{} // TODO(pb): can this be statistically valid?
}

View File

@ -22,6 +22,7 @@ import (
func TestMultiWith(t *testing.T) { func TestMultiWith(t *testing.T) {
c := metrics.NewMultiCounter( c := metrics.NewMultiCounter(
"multifoo",
expvar.NewCounter("foo"), expvar.NewCounter("foo"),
prometheus.NewCounter(stdprometheus.CounterOpts{ prometheus.NewCounter(stdprometheus.CounterOpts{
Namespace: "test", Namespace: "test",
@ -47,6 +48,7 @@ func TestMultiWith(t *testing.T) {
func TestMultiCounter(t *testing.T) { func TestMultiCounter(t *testing.T) {
metrics.NewMultiCounter( metrics.NewMultiCounter(
"multialpha",
expvar.NewCounter("alpha"), expvar.NewCounter("alpha"),
prometheus.NewCounter(stdprometheus.CounterOpts{ prometheus.NewCounter(stdprometheus.CounterOpts{
Namespace: "test", Namespace: "test",
@ -71,6 +73,7 @@ func TestMultiCounter(t *testing.T) {
func TestMultiGauge(t *testing.T) { func TestMultiGauge(t *testing.T) {
g := metrics.NewMultiGauge( g := metrics.NewMultiGauge(
"multidelta",
expvar.NewGauge("delta"), expvar.NewGauge("delta"),
prometheus.NewGauge(stdprometheus.GaugeOpts{ prometheus.NewGauge(stdprometheus.GaugeOpts{
Namespace: "test", Namespace: "test",
@ -111,6 +114,7 @@ func TestMultiGauge(t *testing.T) {
func TestMultiHistogram(t *testing.T) { func TestMultiHistogram(t *testing.T) {
quantiles := []int{50, 90, 99} quantiles := []int{50, 90, 99}
h := metrics.NewMultiHistogram( h := metrics.NewMultiHistogram(
"multiomicron",
expvar.NewHistogram("omicron", 0, 100, 3, quantiles...), expvar.NewHistogram("omicron", 0, 100, 3, quantiles...),
prometheus.NewSummary(stdprometheus.SummaryOpts{ prometheus.NewSummary(stdprometheus.SummaryOpts{
Namespace: "test", Namespace: "test",

39
metrics/print.go Normal file
View File

@ -0,0 +1,39 @@
package metrics
import (
"fmt"
"io"
"text/tabwriter"
)
const (
bs = "####################################################################################################"
bsz = float64(len(bs))
)
// PrintDistribution writes a human-readable graph of the distribution to the
// passed writer.
func PrintDistribution(w io.Writer, name string, buckets []Bucket) {
fmt.Fprintf(w, "name: %v\n", name)
var total float64
for _, bucket := range buckets {
total += float64(bucket.Count)
}
tw := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0)
fmt.Fprintf(tw, "From\tTo\tCount\tProb\tBar\n")
axis := "|"
for _, bucket := range buckets {
if bucket.Count > 0 {
p := float64(bucket.Count) / total
fmt.Fprintf(tw, "%d\t%d\t%d\t%.4f\t%s%s\n", bucket.From, bucket.To, bucket.Count, p, axis, bs[:int(p*bsz)])
axis = "|"
} else {
axis = ":" // show that some bars were skipped
}
}
tw.Flush() // to buf
}

23
metrics/print_test.go Normal file
View File

@ -0,0 +1,23 @@
package metrics_test
import (
"os"
"testing"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/expvar"
"github.com/go-kit/kit/metrics/teststat"
)
func TestPrintDistribution(t *testing.T) {
var (
name = "foobar"
quantiles = []int{50, 90, 95, 99}
h = expvar.NewHistogram("test_print_distribution", 1, 10, 3, quantiles...)
seed = int64(555)
mean = int64(5)
stdev = int64(1)
)
teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
metrics.PrintDistribution(os.Stdout, name, h.Distribution())
}

View File

@ -16,6 +16,7 @@ var PrometheusLabelValueUnknown = "unknown"
type prometheusCounter struct { type prometheusCounter struct {
*prometheus.CounterVec *prometheus.CounterVec
name string
Pairs map[string]string Pairs map[string]string
} }
@ -30,13 +31,17 @@ func NewCounter(opts prometheus.CounterOpts, fieldKeys []string) metrics.Counter
} }
return prometheusCounter{ return prometheusCounter{
CounterVec: m, CounterVec: m,
name: opts.Name,
Pairs: p, Pairs: p,
} }
} }
func (c prometheusCounter) Name() string { return c.name }
func (c prometheusCounter) With(f metrics.Field) metrics.Counter { func (c prometheusCounter) With(f metrics.Field) metrics.Counter {
return prometheusCounter{ return prometheusCounter{
CounterVec: c.CounterVec, CounterVec: c.CounterVec,
name: c.name,
Pairs: merge(c.Pairs, f), Pairs: merge(c.Pairs, f),
} }
} }
@ -47,6 +52,7 @@ func (c prometheusCounter) Add(delta uint64) {
type prometheusGauge struct { type prometheusGauge struct {
*prometheus.GaugeVec *prometheus.GaugeVec
name string
Pairs map[string]string Pairs map[string]string
} }
@ -57,13 +63,17 @@ func NewGauge(opts prometheus.GaugeOpts, fieldKeys []string) metrics.Gauge {
prometheus.MustRegister(m) prometheus.MustRegister(m)
return prometheusGauge{ return prometheusGauge{
GaugeVec: m, GaugeVec: m,
name: opts.Name,
Pairs: pairsFrom(fieldKeys), Pairs: pairsFrom(fieldKeys),
} }
} }
func (g prometheusGauge) Name() string { return g.name }
func (g prometheusGauge) With(f metrics.Field) metrics.Gauge { func (g prometheusGauge) With(f metrics.Field) metrics.Gauge {
return prometheusGauge{ return prometheusGauge{
GaugeVec: g.GaugeVec, GaugeVec: g.GaugeVec,
name: g.name,
Pairs: merge(g.Pairs, f), Pairs: merge(g.Pairs, f),
} }
} }
@ -86,6 +96,7 @@ func RegisterCallbackGauge(opts prometheus.GaugeOpts, callback func() float64) {
type prometheusSummary struct { type prometheusSummary struct {
*prometheus.SummaryVec *prometheus.SummaryVec
name string
Pairs map[string]string Pairs map[string]string
} }
@ -99,13 +110,17 @@ func NewSummary(opts prometheus.SummaryOpts, fieldKeys []string) metrics.Histogr
prometheus.MustRegister(m) prometheus.MustRegister(m)
return prometheusSummary{ return prometheusSummary{
SummaryVec: m, SummaryVec: m,
name: opts.Name,
Pairs: pairsFrom(fieldKeys), Pairs: pairsFrom(fieldKeys),
} }
} }
func (s prometheusSummary) Name() string { return s.name }
func (s prometheusSummary) With(f metrics.Field) metrics.Histogram { func (s prometheusSummary) With(f metrics.Field) metrics.Histogram {
return prometheusSummary{ return prometheusSummary{
SummaryVec: s.SummaryVec, SummaryVec: s.SummaryVec,
name: s.name,
Pairs: merge(s.Pairs, f), Pairs: merge(s.Pairs, f),
} }
} }
@ -114,8 +129,14 @@ func (s prometheusSummary) Observe(value int64) {
s.SummaryVec.With(prometheus.Labels(s.Pairs)).Observe(float64(value)) s.SummaryVec.With(prometheus.Labels(s.Pairs)).Observe(float64(value))
} }
func (s prometheusSummary) Distribution() []metrics.Bucket {
// TODO(pb): see https://github.com/prometheus/client_golang/issues/58
return []metrics.Bucket{}
}
type prometheusHistogram struct { type prometheusHistogram struct {
*prometheus.HistogramVec *prometheus.HistogramVec
name string
Pairs map[string]string Pairs map[string]string
} }
@ -129,13 +150,17 @@ func NewHistogram(opts prometheus.HistogramOpts, fieldKeys []string) metrics.His
prometheus.MustRegister(m) prometheus.MustRegister(m)
return prometheusHistogram{ return prometheusHistogram{
HistogramVec: m, HistogramVec: m,
name: opts.Name,
Pairs: pairsFrom(fieldKeys), Pairs: pairsFrom(fieldKeys),
} }
} }
func (h prometheusHistogram) Name() string { return h.name }
func (h prometheusHistogram) With(f metrics.Field) metrics.Histogram { func (h prometheusHistogram) With(f metrics.Field) metrics.Histogram {
return prometheusHistogram{ return prometheusHistogram{
HistogramVec: h.HistogramVec, HistogramVec: h.HistogramVec,
name: h.name,
Pairs: merge(h.Pairs, f), Pairs: merge(h.Pairs, f),
} }
} }
@ -144,6 +169,11 @@ func (h prometheusHistogram) Observe(value int64) {
h.HistogramVec.With(prometheus.Labels(h.Pairs)).Observe(float64(value)) h.HistogramVec.With(prometheus.Labels(h.Pairs)).Observe(float64(value))
} }
func (h prometheusHistogram) Distribution() []metrics.Bucket {
// TODO(pb): see https://github.com/prometheus/client_golang/issues/58
return []metrics.Bucket{}
}
func pairsFrom(fieldKeys []string) map[string]string { func pairsFrom(fieldKeys []string) map[string]string {
p := map[string]string{} p := map[string]string{}
for _, fieldName := range fieldKeys { for _, fieldName := range fieldKeys {

View File

@ -27,7 +27,10 @@ import (
const maxBufferSize = 1400 // bytes const maxBufferSize = 1400 // bytes
type statsdCounter chan string type statsdCounter struct {
key string
c chan string
}
// NewCounter returns a Counter that emits observations in the statsd protocol // NewCounter returns a Counter that emits observations in the statsd protocol
// to the passed writer. Observations are buffered for the report interval or // to the passed writer. Observations are buffered for the report interval or
@ -36,16 +39,24 @@ type statsdCounter chan string
// //
// TODO: support for sampling. // TODO: support for sampling.
func NewCounter(w io.Writer, key string, reportInterval time.Duration) metrics.Counter { func NewCounter(w io.Writer, key string, reportInterval time.Duration) metrics.Counter {
c := make(chan string) c := &statsdCounter{
go fwd(w, key, reportInterval, c) key: key,
return statsdCounter(c) c: make(chan string),
}
go fwd(w, key, reportInterval, c.c)
return c
} }
func (c statsdCounter) With(metrics.Field) metrics.Counter { return c } func (c *statsdCounter) Name() string { return c.key }
func (c statsdCounter) Add(delta uint64) { c <- fmt.Sprintf("%d|c", delta) } func (c *statsdCounter) With(metrics.Field) metrics.Counter { return c }
type statsdGauge chan string func (c *statsdCounter) Add(delta uint64) { c.c <- fmt.Sprintf("%d|c", delta) }
type statsdGauge struct {
key string
g chan string
}
// NewGauge returns a Gauge that emits values in the statsd protocol to the // NewGauge returns a Gauge that emits values in the statsd protocol to the
// passed writer. Values are buffered for the report interval or until the // passed writer. Values are buffered for the report interval or until the
@ -54,24 +65,29 @@ type statsdGauge chan string
// //
// TODO: support for sampling. // TODO: support for sampling.
func NewGauge(w io.Writer, key string, reportInterval time.Duration) metrics.Gauge { func NewGauge(w io.Writer, key string, reportInterval time.Duration) metrics.Gauge {
g := make(chan string) g := &statsdGauge{
go fwd(w, key, reportInterval, g) key: key,
return statsdGauge(g) g: make(chan string),
}
go fwd(w, key, reportInterval, g.g)
return g
} }
func (g statsdGauge) With(metrics.Field) metrics.Gauge { return g } func (g *statsdGauge) Name() string { return g.key }
func (g statsdGauge) Add(delta float64) { func (g *statsdGauge) With(metrics.Field) metrics.Gauge { return g }
func (g *statsdGauge) Add(delta float64) {
// https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges
sign := "+" sign := "+"
if delta < 0 { if delta < 0 {
sign, delta = "-", -delta sign, delta = "-", -delta
} }
g <- fmt.Sprintf("%s%f|g", sign, delta) g.g <- fmt.Sprintf("%s%f|g", sign, delta)
} }
func (g statsdGauge) Set(value float64) { func (g *statsdGauge) Set(value float64) {
g <- fmt.Sprintf("%f|g", value) g.g <- fmt.Sprintf("%f|g", value)
} }
// NewCallbackGauge emits values in the statsd protocol to the passed writer. // NewCallbackGauge emits values in the statsd protocol to the passed writer.
@ -94,7 +110,10 @@ func emitEvery(d time.Duration, callback func() float64) <-chan string {
return c return c
} }
type statsdHistogram chan string type statsdHistogram struct {
key string
h chan string
}
// NewHistogram returns a Histogram that emits observations in the statsd // NewHistogram returns a Histogram that emits observations in the statsd
// protocol to the passed writer. Observations are buffered for the reporting // protocol to the passed writer. Observations are buffered for the reporting
@ -114,15 +133,25 @@ type statsdHistogram chan string
// //
// TODO: support for sampling. // TODO: support for sampling.
func NewHistogram(w io.Writer, key string, reportInterval time.Duration) metrics.Histogram { func NewHistogram(w io.Writer, key string, reportInterval time.Duration) metrics.Histogram {
h := make(chan string) h := &statsdHistogram{
go fwd(w, key, reportInterval, h) key: key,
return statsdHistogram(h) h: make(chan string),
}
go fwd(w, key, reportInterval, h.h)
return h
} }
func (h statsdHistogram) With(metrics.Field) metrics.Histogram { return h } func (h *statsdHistogram) Name() string { return h.key }
func (h statsdHistogram) Observe(value int64) { func (h *statsdHistogram) With(metrics.Field) metrics.Histogram { return h }
h <- fmt.Sprintf("%d|ms", value)
func (h *statsdHistogram) Observe(value int64) {
h.h <- fmt.Sprintf("%d|ms", value)
}
func (h *statsdHistogram) Distribution() []metrics.Bucket {
// TODO(pb): no way to do this without introducing e.g. codahale/hdrhistogram
return []metrics.Bucket{}
} }
var tick = time.Tick var tick = time.Tick