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
Peter Bourgon
7 years ago
12 | 12 | |
13 | 13 | ## Rationale |
14 | 14 | |
15 | TODO | |
15 | Code instrumentation is absolutely essential to achieve [observability][] into a distributed system. | |
16 | Metrics and instrumentation tools have coalesced around a few well-defined idioms. | |
17 | `package metrics` provides a common, minimal interface those idioms for service authors. | |
18 | ||
19 | [observability]: https://speakerdeck.com/mattheath/observability-in-micro-service-architectures | |
16 | 20 | |
17 | 21 | ## Usage |
18 | 22 | |
52 | 56 | ``` |
53 | 57 | |
54 | 58 | A gauge for the number of goroutines currently running, exported via statsd. |
59 | ||
55 | 60 | ```go |
56 | 61 | import ( |
57 | 62 | "net" |
65 | 70 | func main() { |
66 | 71 | statsdWriter, err := net.Dial("udp", "127.0.0.1:8126") |
67 | 72 | if err != nil { |
68 | os.Exit(1) | |
73 | panic(err) | |
69 | 74 | } |
70 | 75 | |
71 | reportingDuration := 5 * time.Second | |
72 | goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportingDuration) | |
73 | for range time.Tick(reportingDuration) { | |
76 | reportInterval := 5 * time.Second | |
77 | goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportInterval) | |
78 | for range time.Tick(reportInterval) { | |
74 | 79 | goroutines.Set(float64(runtime.NumGoroutine())) |
75 | 80 | } |
76 | 81 | } |
77 | ||
78 | 82 | ``` |
28 | 28 | ) |
29 | 29 | |
30 | 30 | type counter struct { |
31 | v *expvar.Int | |
31 | name string | |
32 | v *expvar.Int | |
32 | 33 | } |
33 | 34 | |
34 | 35 | // NewCounter returns a new Counter backed by an expvar with the given name. |
35 | 36 | // Fields are ignored. |
36 | 37 | func NewCounter(name string) metrics.Counter { |
37 | return &counter{expvar.NewInt(name)} | |
38 | return &counter{ | |
39 | name: name, | |
40 | v: expvar.NewInt(name), | |
41 | } | |
38 | 42 | } |
39 | 43 | |
44 | func (c *counter) Name() string { return c.name } | |
40 | 45 | func (c *counter) With(metrics.Field) metrics.Counter { return c } |
41 | 46 | func (c *counter) Add(delta uint64) { c.v.Add(int64(delta)) } |
42 | 47 | |
43 | 48 | type gauge struct { |
44 | v *expvar.Float | |
49 | name string | |
50 | v *expvar.Float | |
45 | 51 | } |
46 | 52 | |
47 | 53 | // NewGauge returns a new Gauge backed by an expvar with the given name. It |
48 | 54 | // should be updated manually; for a callback-based approach, see |
49 | 55 | // PublishCallbackGauge. Fields are ignored. |
50 | 56 | func NewGauge(name string) metrics.Gauge { |
51 | return &gauge{expvar.NewFloat(name)} | |
57 | return &gauge{ | |
58 | name: name, | |
59 | v: expvar.NewFloat(name), | |
60 | } | |
52 | 61 | } |
53 | 62 | |
63 | func (g *gauge) Name() string { return g.name } | |
54 | 64 | func (g *gauge) With(metrics.Field) metrics.Gauge { return g } |
55 | ||
56 | func (g *gauge) Add(delta float64) { g.v.Add(delta) } | |
57 | ||
58 | func (g *gauge) Set(value float64) { g.v.Set(value) } | |
65 | func (g *gauge) Add(delta float64) { g.v.Add(delta) } | |
66 | func (g *gauge) Set(value float64) { g.v.Set(value) } | |
59 | 67 | |
60 | 68 | // PublishCallbackGauge publishes a Gauge as an expvar with the given name, |
61 | 69 | // whose value is determined at collect time by the passed callback function. |
100 | 108 | return h |
101 | 109 | } |
102 | 110 | |
111 | func (h *histogram) Name() string { return h.name } | |
103 | 112 | func (h *histogram) With(metrics.Field) metrics.Histogram { return h } |
104 | 113 | |
105 | 114 | func (h *histogram) Observe(value int64) { |
116 | 125 | } |
117 | 126 | } |
118 | 127 | |
128 | func (h *histogram) Distribution() []metrics.Bucket { | |
129 | bars := h.hist.Current.Distribution() | |
130 | buckets := make([]metrics.Bucket, len(bars)) | |
131 | for i, bar := range bars { | |
132 | buckets[i] = metrics.Bucket{ | |
133 | From: bar.From, | |
134 | To: bar.To, | |
135 | Count: bar.Count, | |
136 | } | |
137 | } | |
138 | return buckets | |
139 | } | |
140 | ||
119 | 141 | func (h *histogram) rotateLoop(d time.Duration) { |
120 | 142 | for range time.Tick(d) { |
121 | 143 | h.mu.Lock() |
11 | 11 | |
12 | 12 | func TestHistogramQuantiles(t *testing.T) { |
13 | 13 | var ( |
14 | name = "test_histogram" | |
14 | name = "test_histogram_quantiles" | |
15 | 15 | quantiles = []int{50, 90, 95, 99} |
16 | 16 | h = expvar.NewHistogram(name, 0, 100, 3, quantiles...).With(metrics.Field{Key: "ignored", Value: "field"}) |
17 | 17 | ) |
8 | 8 | // between measurements of a counter over intervals of time, an aggregation |
9 | 9 | // layer can derive rates, acceleration, etc. |
10 | 10 | type Counter interface { |
11 | Name() string | |
11 | 12 | With(Field) Counter |
12 | 13 | Add(delta uint64) |
13 | 14 | } |
15 | 16 | // Gauge captures instantaneous measurements of something using signed, 64-bit |
16 | 17 | // floats. The value does not need to be monotonic. |
17 | 18 | type Gauge interface { |
19 | Name() string | |
18 | 20 | With(Field) Gauge |
19 | 21 | Set(value float64) |
20 | 22 | Add(delta float64) |
24 | 26 | // milliseconds it takes to handle requests). Implementations may choose to |
25 | 27 | // add gauges for values at meaningful quantiles. |
26 | 28 | type Histogram interface { |
29 | Name() string | |
27 | 30 | With(Field) Histogram |
28 | 31 | Observe(value int64) |
32 | Distribution() []Bucket | |
29 | 33 | } |
30 | 34 | |
31 | 35 | // Field is a key/value pair associated with an observation for a specific |
34 | 38 | Key string |
35 | 39 | Value string |
36 | 40 | } |
41 | ||
42 | // Bucket is a range in a histogram which aggregates observations. | |
43 | type Bucket struct { | |
44 | From int64 | |
45 | To int64 | |
46 | Count int64 | |
47 | } |
0 | 0 | package metrics |
1 | 1 | |
2 | type multiCounter []Counter | |
2 | type multiCounter struct { | |
3 | name string | |
4 | a []Counter | |
5 | } | |
3 | 6 | |
4 | 7 | // NewMultiCounter returns a wrapper around multiple Counters. |
5 | func NewMultiCounter(counters ...Counter) Counter { | |
6 | c := make(multiCounter, 0, len(counters)) | |
7 | return append(c, counters...) | |
8 | func NewMultiCounter(name string, counters ...Counter) Counter { | |
9 | return &multiCounter{ | |
10 | name: name, | |
11 | a: counters, | |
12 | } | |
8 | 13 | } |
9 | 14 | |
15 | func (c multiCounter) Name() string { return c.name } | |
16 | ||
10 | 17 | func (c multiCounter) With(f Field) Counter { |
11 | next := make(multiCounter, len(c)) | |
12 | for i, counter := range c { | |
13 | next[i] = counter.With(f) | |
18 | next := &multiCounter{ | |
19 | name: c.name, | |
20 | a: make([]Counter, len(c.a)), | |
21 | } | |
22 | for i, counter := range c.a { | |
23 | next.a[i] = counter.With(f) | |
14 | 24 | } |
15 | 25 | return next |
16 | 26 | } |
17 | 27 | |
18 | 28 | func (c multiCounter) Add(delta uint64) { |
19 | for _, counter := range c { | |
29 | for _, counter := range c.a { | |
20 | 30 | counter.Add(delta) |
21 | 31 | } |
22 | 32 | } |
23 | 33 | |
24 | type multiGauge []Gauge | |
34 | type multiGauge struct { | |
35 | name string | |
36 | a []Gauge | |
37 | } | |
38 | ||
39 | func (g multiGauge) Name() string { return g.name } | |
25 | 40 | |
26 | 41 | // NewMultiGauge returns a wrapper around multiple Gauges. |
27 | func NewMultiGauge(gauges ...Gauge) Gauge { | |
28 | g := make(multiGauge, 0, len(gauges)) | |
29 | return append(g, gauges...) | |
42 | func NewMultiGauge(name string, gauges ...Gauge) Gauge { | |
43 | return &multiGauge{ | |
44 | name: name, | |
45 | a: gauges, | |
46 | } | |
30 | 47 | } |
31 | 48 | |
32 | 49 | func (g multiGauge) With(f Field) Gauge { |
33 | next := make(multiGauge, len(g)) | |
34 | for i, gauge := range g { | |
35 | next[i] = gauge.With(f) | |
50 | next := &multiGauge{ | |
51 | name: g.name, | |
52 | a: make([]Gauge, len(g.a)), | |
53 | } | |
54 | for i, gauge := range g.a { | |
55 | next.a[i] = gauge.With(f) | |
36 | 56 | } |
37 | 57 | return next |
38 | 58 | } |
39 | 59 | |
40 | 60 | func (g multiGauge) Set(value float64) { |
41 | for _, gauge := range g { | |
61 | for _, gauge := range g.a { | |
42 | 62 | gauge.Set(value) |
43 | 63 | } |
44 | 64 | } |
45 | 65 | |
46 | 66 | func (g multiGauge) Add(delta float64) { |
47 | for _, gauge := range g { | |
67 | for _, gauge := range g.a { | |
48 | 68 | gauge.Add(delta) |
49 | 69 | } |
50 | 70 | } |
51 | 71 | |
52 | type multiHistogram []Histogram | |
72 | type multiHistogram struct { | |
73 | name string | |
74 | a []Histogram | |
75 | } | |
53 | 76 | |
54 | 77 | // NewMultiHistogram returns a wrapper around multiple Histograms. |
55 | func NewMultiHistogram(histograms ...Histogram) Histogram { | |
56 | h := make(multiHistogram, 0, len(histograms)) | |
57 | return append(h, histograms...) | |
78 | func NewMultiHistogram(name string, histograms ...Histogram) Histogram { | |
79 | return &multiHistogram{ | |
80 | name: name, | |
81 | a: histograms, | |
82 | } | |
58 | 83 | } |
59 | 84 | |
85 | func (h multiHistogram) Name() string { return h.name } | |
86 | ||
60 | 87 | func (h multiHistogram) With(f Field) Histogram { |
61 | next := make(multiHistogram, len(h)) | |
62 | for i, histogram := range h { | |
63 | next[i] = histogram.With(f) | |
88 | next := &multiHistogram{ | |
89 | name: h.name, | |
90 | a: make([]Histogram, len(h.a)), | |
91 | } | |
92 | for i, histogram := range h.a { | |
93 | next.a[i] = histogram.With(f) | |
64 | 94 | } |
65 | 95 | return next |
66 | 96 | } |
67 | 97 | |
68 | 98 | func (h multiHistogram) Observe(value int64) { |
69 | for _, histogram := range h { | |
99 | for _, histogram := range h.a { | |
70 | 100 | histogram.Observe(value) |
71 | 101 | } |
72 | 102 | } |
103 | ||
104 | func (h multiHistogram) Distribution() []Bucket { | |
105 | return []Bucket{} // TODO(pb): can this be statistically valid? | |
106 | } |
21 | 21 | |
22 | 22 | func TestMultiWith(t *testing.T) { |
23 | 23 | c := metrics.NewMultiCounter( |
24 | "multifoo", | |
24 | 25 | expvar.NewCounter("foo"), |
25 | 26 | prometheus.NewCounter(stdprometheus.CounterOpts{ |
26 | 27 | Namespace: "test", |
46 | 47 | |
47 | 48 | func TestMultiCounter(t *testing.T) { |
48 | 49 | metrics.NewMultiCounter( |
50 | "multialpha", | |
49 | 51 | expvar.NewCounter("alpha"), |
50 | 52 | prometheus.NewCounter(stdprometheus.CounterOpts{ |
51 | 53 | Namespace: "test", |
70 | 72 | |
71 | 73 | func TestMultiGauge(t *testing.T) { |
72 | 74 | g := metrics.NewMultiGauge( |
75 | "multidelta", | |
73 | 76 | expvar.NewGauge("delta"), |
74 | 77 | prometheus.NewGauge(stdprometheus.GaugeOpts{ |
75 | 78 | Namespace: "test", |
110 | 113 | func TestMultiHistogram(t *testing.T) { |
111 | 114 | quantiles := []int{50, 90, 99} |
112 | 115 | h := metrics.NewMultiHistogram( |
116 | "multiomicron", | |
113 | 117 | expvar.NewHistogram("omicron", 0, 100, 3, quantiles...), |
114 | 118 | prometheus.NewSummary(stdprometheus.SummaryOpts{ |
115 | 119 | Namespace: "test", |
0 | package metrics | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | "io" | |
5 | "text/tabwriter" | |
6 | ) | |
7 | ||
8 | const ( | |
9 | bs = "####################################################################################################" | |
10 | bsz = float64(len(bs)) | |
11 | ) | |
12 | ||
13 | // PrintDistribution writes a human-readable graph of the distribution to the | |
14 | // passed writer. | |
15 | func PrintDistribution(w io.Writer, name string, buckets []Bucket) { | |
16 | fmt.Fprintf(w, "name: %v\n", name) | |
17 | ||
18 | var total float64 | |
19 | for _, bucket := range buckets { | |
20 | total += float64(bucket.Count) | |
21 | } | |
22 | ||
23 | tw := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0) | |
24 | fmt.Fprintf(tw, "From\tTo\tCount\tProb\tBar\n") | |
25 | ||
26 | axis := "|" | |
27 | for _, bucket := range buckets { | |
28 | if bucket.Count > 0 { | |
29 | p := float64(bucket.Count) / total | |
30 | 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)]) | |
31 | axis = "|" | |
32 | } else { | |
33 | axis = ":" // show that some bars were skipped | |
34 | } | |
35 | } | |
36 | ||
37 | tw.Flush() // to buf | |
38 | } |
0 | package metrics_test | |
1 | ||
2 | import ( | |
3 | "os" | |
4 | "testing" | |
5 | ||
6 | "github.com/go-kit/kit/metrics" | |
7 | "github.com/go-kit/kit/metrics/expvar" | |
8 | "github.com/go-kit/kit/metrics/teststat" | |
9 | ) | |
10 | ||
11 | func TestPrintDistribution(t *testing.T) { | |
12 | var ( | |
13 | name = "foobar" | |
14 | quantiles = []int{50, 90, 95, 99} | |
15 | h = expvar.NewHistogram("test_print_distribution", 1, 10, 3, quantiles...) | |
16 | seed = int64(555) | |
17 | mean = int64(5) | |
18 | stdev = int64(1) | |
19 | ) | |
20 | teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) | |
21 | metrics.PrintDistribution(os.Stdout, name, h.Distribution()) | |
22 | } |
15 | 15 | |
16 | 16 | type prometheusCounter struct { |
17 | 17 | *prometheus.CounterVec |
18 | name string | |
18 | 19 | Pairs map[string]string |
19 | 20 | } |
20 | 21 | |
29 | 30 | } |
30 | 31 | return prometheusCounter{ |
31 | 32 | CounterVec: m, |
33 | name: opts.Name, | |
32 | 34 | Pairs: p, |
33 | 35 | } |
34 | 36 | } |
35 | 37 | |
38 | func (c prometheusCounter) Name() string { return c.name } | |
39 | ||
36 | 40 | func (c prometheusCounter) With(f metrics.Field) metrics.Counter { |
37 | 41 | return prometheusCounter{ |
38 | 42 | CounterVec: c.CounterVec, |
43 | name: c.name, | |
39 | 44 | Pairs: merge(c.Pairs, f), |
40 | 45 | } |
41 | 46 | } |
46 | 51 | |
47 | 52 | type prometheusGauge struct { |
48 | 53 | *prometheus.GaugeVec |
54 | name string | |
49 | 55 | Pairs map[string]string |
50 | 56 | } |
51 | 57 | |
56 | 62 | prometheus.MustRegister(m) |
57 | 63 | return prometheusGauge{ |
58 | 64 | GaugeVec: m, |
65 | name: opts.Name, | |
59 | 66 | Pairs: pairsFrom(fieldKeys), |
60 | 67 | } |
61 | 68 | } |
62 | 69 | |
70 | func (g prometheusGauge) Name() string { return g.name } | |
71 | ||
63 | 72 | func (g prometheusGauge) With(f metrics.Field) metrics.Gauge { |
64 | 73 | return prometheusGauge{ |
65 | 74 | GaugeVec: g.GaugeVec, |
75 | name: g.name, | |
66 | 76 | Pairs: merge(g.Pairs, f), |
67 | 77 | } |
68 | 78 | } |
85 | 95 | |
86 | 96 | type prometheusSummary struct { |
87 | 97 | *prometheus.SummaryVec |
98 | name string | |
88 | 99 | Pairs map[string]string |
89 | 100 | } |
90 | 101 | |
98 | 109 | prometheus.MustRegister(m) |
99 | 110 | return prometheusSummary{ |
100 | 111 | SummaryVec: m, |
112 | name: opts.Name, | |
101 | 113 | Pairs: pairsFrom(fieldKeys), |
102 | 114 | } |
103 | 115 | } |
104 | 116 | |
117 | func (s prometheusSummary) Name() string { return s.name } | |
118 | ||
105 | 119 | func (s prometheusSummary) With(f metrics.Field) metrics.Histogram { |
106 | 120 | return prometheusSummary{ |
107 | 121 | SummaryVec: s.SummaryVec, |
122 | name: s.name, | |
108 | 123 | Pairs: merge(s.Pairs, f), |
109 | 124 | } |
110 | 125 | } |
113 | 128 | s.SummaryVec.With(prometheus.Labels(s.Pairs)).Observe(float64(value)) |
114 | 129 | } |
115 | 130 | |
131 | func (s prometheusSummary) Distribution() []metrics.Bucket { | |
132 | // TODO(pb): see https://github.com/prometheus/client_golang/issues/58 | |
133 | return []metrics.Bucket{} | |
134 | } | |
135 | ||
116 | 136 | type prometheusHistogram struct { |
117 | 137 | *prometheus.HistogramVec |
138 | name string | |
118 | 139 | Pairs map[string]string |
119 | 140 | } |
120 | 141 | |
128 | 149 | prometheus.MustRegister(m) |
129 | 150 | return prometheusHistogram{ |
130 | 151 | HistogramVec: m, |
152 | name: opts.Name, | |
131 | 153 | Pairs: pairsFrom(fieldKeys), |
132 | 154 | } |
133 | 155 | } |
134 | 156 | |
157 | func (h prometheusHistogram) Name() string { return h.name } | |
158 | ||
135 | 159 | func (h prometheusHistogram) With(f metrics.Field) metrics.Histogram { |
136 | 160 | return prometheusHistogram{ |
137 | 161 | HistogramVec: h.HistogramVec, |
162 | name: h.name, | |
138 | 163 | Pairs: merge(h.Pairs, f), |
139 | 164 | } |
140 | 165 | } |
141 | 166 | |
142 | 167 | func (h prometheusHistogram) Observe(value int64) { |
143 | 168 | h.HistogramVec.With(prometheus.Labels(h.Pairs)).Observe(float64(value)) |
169 | } | |
170 | ||
171 | func (h prometheusHistogram) Distribution() []metrics.Bucket { | |
172 | // TODO(pb): see https://github.com/prometheus/client_golang/issues/58 | |
173 | return []metrics.Bucket{} | |
144 | 174 | } |
145 | 175 | |
146 | 176 | func pairsFrom(fieldKeys []string) map[string]string { |
26 | 26 | |
27 | 27 | const maxBufferSize = 1400 // bytes |
28 | 28 | |
29 | type statsdCounter chan string | |
29 | type statsdCounter struct { | |
30 | key string | |
31 | c chan string | |
32 | } | |
30 | 33 | |
31 | 34 | // NewCounter returns a Counter that emits observations in the statsd protocol |
32 | 35 | // to the passed writer. Observations are buffered for the report interval or |
35 | 38 | // |
36 | 39 | // TODO: support for sampling. |
37 | 40 | func NewCounter(w io.Writer, key string, reportInterval time.Duration) metrics.Counter { |
38 | c := make(chan string) | |
39 | go fwd(w, key, reportInterval, c) | |
40 | return statsdCounter(c) | |
41 | c := &statsdCounter{ | |
42 | key: key, | |
43 | c: make(chan string), | |
44 | } | |
45 | go fwd(w, key, reportInterval, c.c) | |
46 | return c | |
41 | 47 | } |
42 | 48 | |
43 | func (c statsdCounter) With(metrics.Field) metrics.Counter { return c } | |
49 | func (c *statsdCounter) Name() string { return c.key } | |
44 | 50 | |
45 | func (c statsdCounter) Add(delta uint64) { c <- fmt.Sprintf("%d|c", delta) } | |
51 | func (c *statsdCounter) With(metrics.Field) metrics.Counter { return c } | |
46 | 52 | |
47 | type statsdGauge chan string | |
53 | func (c *statsdCounter) Add(delta uint64) { c.c <- fmt.Sprintf("%d|c", delta) } | |
54 | ||
55 | type statsdGauge struct { | |
56 | key string | |
57 | g chan string | |
58 | } | |
48 | 59 | |
49 | 60 | // NewGauge returns a Gauge that emits values in the statsd protocol to the |
50 | 61 | // passed writer. Values are buffered for the report interval or until the |
53 | 64 | // |
54 | 65 | // TODO: support for sampling. |
55 | 66 | func NewGauge(w io.Writer, key string, reportInterval time.Duration) metrics.Gauge { |
56 | g := make(chan string) | |
57 | go fwd(w, key, reportInterval, g) | |
58 | return statsdGauge(g) | |
67 | g := &statsdGauge{ | |
68 | key: key, | |
69 | g: make(chan string), | |
70 | } | |
71 | go fwd(w, key, reportInterval, g.g) | |
72 | return g | |
59 | 73 | } |
60 | 74 | |
61 | func (g statsdGauge) With(metrics.Field) metrics.Gauge { return g } | |
75 | func (g *statsdGauge) Name() string { return g.key } | |
62 | 76 | |
63 | func (g statsdGauge) Add(delta float64) { | |
77 | func (g *statsdGauge) With(metrics.Field) metrics.Gauge { return g } | |
78 | ||
79 | func (g *statsdGauge) Add(delta float64) { | |
64 | 80 | // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges |
65 | 81 | sign := "+" |
66 | 82 | if delta < 0 { |
67 | 83 | sign, delta = "-", -delta |
68 | 84 | } |
69 | g <- fmt.Sprintf("%s%f|g", sign, delta) | |
85 | g.g <- fmt.Sprintf("%s%f|g", sign, delta) | |
70 | 86 | } |
71 | 87 | |
72 | func (g statsdGauge) Set(value float64) { | |
73 | g <- fmt.Sprintf("%f|g", value) | |
88 | func (g *statsdGauge) Set(value float64) { | |
89 | g.g <- fmt.Sprintf("%f|g", value) | |
74 | 90 | } |
75 | 91 | |
76 | 92 | // NewCallbackGauge emits values in the statsd protocol to the passed writer. |
93 | 109 | return c |
94 | 110 | } |
95 | 111 | |
96 | type statsdHistogram chan string | |
112 | type statsdHistogram struct { | |
113 | key string | |
114 | h chan string | |
115 | } | |
97 | 116 | |
98 | 117 | // NewHistogram returns a Histogram that emits observations in the statsd |
99 | 118 | // protocol to the passed writer. Observations are buffered for the reporting |
113 | 132 | // |
114 | 133 | // TODO: support for sampling. |
115 | 134 | func NewHistogram(w io.Writer, key string, reportInterval time.Duration) metrics.Histogram { |
116 | h := make(chan string) | |
117 | go fwd(w, key, reportInterval, h) | |
118 | return statsdHistogram(h) | |
135 | h := &statsdHistogram{ | |
136 | key: key, | |
137 | h: make(chan string), | |
138 | } | |
139 | go fwd(w, key, reportInterval, h.h) | |
140 | return h | |
119 | 141 | } |
120 | 142 | |
121 | func (h statsdHistogram) With(metrics.Field) metrics.Histogram { return h } | |
143 | func (h *statsdHistogram) Name() string { return h.key } | |
122 | 144 | |
123 | func (h statsdHistogram) Observe(value int64) { | |
124 | h <- fmt.Sprintf("%d|ms", value) | |
145 | func (h *statsdHistogram) With(metrics.Field) metrics.Histogram { return h } | |
146 | ||
147 | func (h *statsdHistogram) Observe(value int64) { | |
148 | h.h <- fmt.Sprintf("%d|ms", value) | |
149 | } | |
150 | ||
151 | func (h *statsdHistogram) Distribution() []metrics.Bucket { | |
152 | // TODO(pb): no way to do this without introducing e.g. codahale/hdrhistogram | |
153 | return []metrics.Bucket{} | |
125 | 154 | } |
126 | 155 | |
127 | 156 | var tick = time.Tick |