Codebase list golang-github-go-kit-kit / 8cce994
mv metrics3 metrics Peter Bourgon 7 years ago
98 changed file(s) with 3233 addition(s) and 7487 deletion(s). Raw diff Collapse all Expand all
00 # package metrics
11
22 `package metrics` provides a set of uniform interfaces for service instrumentation.
3 It has **[counters][]**, **[gauges][]**, and **[histograms][]**,
4 and provides adapters to popular metrics packages, like **[expvar][]**, **[statsd][]**, and **[Prometheus][]**.
5
6 [counters]: http://prometheus.io/docs/concepts/metric_types/#counter
7 [gauges]: http://prometheus.io/docs/concepts/metric_types/#gauge
8 [histograms]: http://prometheus.io/docs/concepts/metric_types/#histogram
9 [expvar]: https://golang.org/pkg/expvar
10 [statsd]: https://github.com/etsy/statsd
11 [Prometheus]: http://prometheus.io
3 It has
4 [counters](http://prometheus.io/docs/concepts/metric_types/#counter),
5 [gauges](http://prometheus.io/docs/concepts/metric_types/#gauge), and
6 [histograms](http://prometheus.io/docs/concepts/metric_types/#histogram),
7 and provides adapters to popular metrics packages, like
8 [expvar](https://golang.org/pkg/expvar),
9 [StatsD](https://github.com/etsy/statsd), and
10 [Prometheus](https://prometheus.io).
1211
1312 ## Rationale
1413
15 Code instrumentation is absolutely essential to achieve [observability][] into a distributed system.
14 Code instrumentation is absolutely essential to achieve
15 [observability](https://speakerdeck.com/mattheath/observability-in-micro-service-architectures)
16 into a distributed system.
1617 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
18 `package metrics` provides a common, minimal interface those idioms for service authors.
2019
2120 ## Usage
2221
3130 }
3231 ```
3332
34 A histogram for request duration, exported via a Prometheus summary with
35 dynamically-computed quantiles.
33 A histogram for request duration,
34 exported via a Prometheus summary with dynamically-computed quantiles.
3635
3736 ```go
3837 import (
4241 "github.com/go-kit/kit/metrics/prometheus"
4342 )
4443
45 var requestDuration = prometheus.NewSummary(stdprometheus.SummaryOpts{
44 var dur = prometheus.NewSummary(stdprometheus.SummaryOpts{
4645 Namespace: "myservice",
4746 Subsystem: "api",
48 Name: "request_duration_nanoseconds_count",
49 Help: "Total time spent serving requests.",
47 Name: "request_duration_seconds",
48 Help: "Total time spent serving requests.",
5049 }, []string{})
5150
5251 func handleRequest() {
53 defer func(begin time.Time) { requestDuration.Observe(time.Since(begin)) }(time.Now())
52 defer func(begin time.Time) { dur.Observe(time.Since(begin).Seconds()) }(time.Now())
5453 // handle request
5554 }
5655 ```
5756
58 A gauge for the number of goroutines currently running, exported via statsd.
57 A gauge for the number of goroutines currently running, exported via StatsD.
5958
6059 ```go
6160 import (
6564 "time"
6665
6766 "github.com/go-kit/kit/metrics/statsd"
67 "github.com/go-kit/kit/log"
6868 )
6969
7070 func main() {
71 statsdWriter, err := net.Dial("udp", "127.0.0.1:8126")
72 if err != nil {
73 panic(err)
74 }
71 statsd := statsd.New("foo_svc.", log.NewNopLogger())
7572
76 reportInterval := 5 * time.Second
77 goroutines := statsd.NewGauge(statsdWriter, "total_goroutines", reportInterval)
78 for range time.Tick(reportInterval) {
73 report := time.NewTicker(5*time.Second)
74 defer report.Stop()
75 go statsd.SendLoop(report.C, "tcp", "statsd.internal:8125")
76
77 goroutines := statsd.NewGauge("goroutine_count")
78 for range time.Tick(time.Second) {
7979 goroutines.Set(float64(runtime.NumGoroutine()))
8080 }
8181 }
0 // Package circonus provides a Circonus backend for metrics.
1 package circonus
2
3 import (
4 "github.com/circonus-labs/circonus-gometrics"
5
6 "github.com/go-kit/kit/metrics3"
7 )
8
9 // Circonus wraps a CirconusMetrics object and provides constructors for each of
10 // the Go kit metrics. The CirconusMetrics object manages aggregation of
11 // observations and emission to the Circonus server.
12 type Circonus struct {
13 m *circonusgometrics.CirconusMetrics
14 }
15
16 // New creates a new Circonus object wrapping the passed CirconusMetrics, which
17 // the caller should create and set in motion. The Circonus object can be used
18 // to construct individual Go kit metrics.
19 func New(m *circonusgometrics.CirconusMetrics) *Circonus {
20 return &Circonus{
21 m: m,
22 }
23 }
24
25 // NewCounter returns a counter metric with the given name.
26 func (c *Circonus) NewCounter(name string) *Counter {
27 return &Counter{
28 name: name,
29 m: c.m,
30 }
31 }
32
33 // NewGauge returns a gauge metric with the given name.
34 func (c *Circonus) NewGauge(name string) *Gauge {
35 return &Gauge{
36 name: name,
37 m: c.m,
38 }
39 }
40
41 // NewHistogram returns a histogram metric with the given name.
42 func (c *Circonus) NewHistogram(name string) *Histogram {
43 return &Histogram{
44 h: c.m.NewHistogram(name),
45 }
46 }
47
48 // Counter is a Circonus implementation of a counter metric.
49 type Counter struct {
50 name string
51 m *circonusgometrics.CirconusMetrics
52 }
53
54 // With implements Counter, but is a no-op, because Circonus metrics have no
55 // concept of per-observation label values.
56 func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
57
58 // Add implements Counter. Delta is converted to uint64; precision will be lost.
59 func (c *Counter) Add(delta float64) { c.m.Add(c.name, uint64(delta)) }
60
61 // Gauge is a Circonus implementation of a gauge metric.
62 type Gauge struct {
63 name string
64 m *circonusgometrics.CirconusMetrics
65 }
66
67 // With implements Gauge, but is a no-op, because Circonus metrics have no
68 // concept of per-observation label values.
69 func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
70
71 // Set implements Gauge.
72 func (g *Gauge) Set(value float64) { g.m.SetGauge(g.name, value) }
73
74 // Histogram is a Circonus implementation of a histogram metric.
75 type Histogram struct {
76 h *circonusgometrics.Histogram
77 }
78
79 // With implements Histogram, but is a no-op, because Circonus metrics have no
80 // concept of per-observation label values.
81 func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
82
83 // Observe implements Histogram. No precision is lost.
84 func (h *Histogram) Observe(value float64) { h.h.RecordValue(value) }
0 package circonus
1
2 import (
3 "encoding/json"
4 "net/http"
5 "net/http/httptest"
6 "regexp"
7 "strconv"
8 "testing"
9
10 "github.com/circonus-labs/circonus-gometrics"
11 "github.com/circonus-labs/circonus-gometrics/checkmgr"
12
13 "github.com/go-kit/kit/metrics3/generic"
14 "github.com/go-kit/kit/metrics3/teststat"
15 )
16
17 func TestCounter(t *testing.T) {
18 // The only way to extract values from Circonus is to pose as a Circonus
19 // server and receive real HTTP writes.
20 const name = "abc"
21 var val int64
22 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 var res map[string]struct {
24 Value int64 `json:"_value"` // reverse-engineered :\
25 }
26 json.NewDecoder(r.Body).Decode(&res)
27 val = res[name].Value
28 }))
29 defer s.Close()
30
31 // Set up a Circonus object, submitting to our HTTP server.
32 m := newCirconusMetrics(s.URL)
33 counter := New(m).NewCounter(name).With("label values", "not supported")
34 value := func() float64 { m.Flush(); return float64(val) }
35
36 // Engage.
37 if err := teststat.TestCounter(counter, value); err != nil {
38 t.Fatal(err)
39 }
40 }
41
42 func TestGauge(t *testing.T) {
43 const name = "def"
44 var val float64
45 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46 var res map[string]struct {
47 Value string `json:"_value"`
48 }
49 json.NewDecoder(r.Body).Decode(&res)
50 val, _ = strconv.ParseFloat(res[name].Value, 64)
51 }))
52 defer s.Close()
53
54 m := newCirconusMetrics(s.URL)
55 gauge := New(m).NewGauge(name).With("label values", "not supported")
56 value := func() float64 { m.Flush(); return val }
57
58 if err := teststat.TestGauge(gauge, value); err != nil {
59 t.Fatal(err)
60 }
61 }
62
63 func TestHistogram(t *testing.T) {
64 const name = "ghi"
65
66 // Circonus just emits bucketed counts. We'll dump them into a generic
67 // histogram (losing some precision) and take statistics from there. Note
68 // this does assume that the generic histogram computes statistics properly,
69 // but we have another test for that :)
70 re := regexp.MustCompile(`^H\[([0-9\.e\+]+)\]=([0-9]+)$`) // H[1.2e+03]=456
71
72 var p50, p90, p95, p99 float64
73 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74 var res map[string]struct {
75 Values []string `json:"_value"` // reverse-engineered :\
76 }
77 json.NewDecoder(r.Body).Decode(&res)
78
79 h := generic.NewHistogram("dummy", len(res[name].Values)) // match tbe bucket counts
80 for _, v := range res[name].Values {
81 match := re.FindStringSubmatch(v)
82 f, _ := strconv.ParseFloat(match[1], 64)
83 n, _ := strconv.ParseInt(match[2], 10, 64)
84 for i := int64(0); i < n; i++ {
85 h.Observe(f)
86 }
87 }
88
89 p50 = h.Quantile(0.50)
90 p90 = h.Quantile(0.90)
91 p95 = h.Quantile(0.95)
92 p99 = h.Quantile(0.99)
93 }))
94 defer s.Close()
95
96 m := newCirconusMetrics(s.URL)
97 histogram := New(m).NewHistogram(name).With("label values", "not supported")
98 quantiles := func() (float64, float64, float64, float64) { m.Flush(); return p50, p90, p95, p99 }
99
100 // Circonus metrics, because they do their own bucketing, are less precise
101 // than other systems. So, we bump the tolerance to 5 percent.
102 if err := teststat.TestHistogram(histogram, quantiles, 0.05); err != nil {
103 t.Fatal(err)
104 }
105 }
106
107 func newCirconusMetrics(url string) *circonusgometrics.CirconusMetrics {
108 m, err := circonusgometrics.NewCirconusMetrics(&circonusgometrics.Config{
109 CheckManager: checkmgr.Config{
110 Check: checkmgr.CheckConfig{
111 SubmissionURL: url,
112 },
113 },
114 })
115 if err != nil {
116 panic(err)
117 }
118 return m
119 }
0 // Package discard implements a backend for package metrics that succeeds
1 // without doing anything.
0 // Package discard provides a no-op metrics backend.
21 package discard
32
4 import "github.com/go-kit/kit/metrics"
3 import "github.com/go-kit/kit/metrics3"
54
6 type counter struct {
7 name string
8 }
5 type counter struct{}
96
10 // NewCounter returns a Counter that does nothing.
11 func NewCounter(name string) metrics.Counter { return &counter{name} }
7 // NewCounter returns a new no-op counter.
8 func NewCounter() metrics.Counter { return counter{} }
129
13 func (c *counter) Name() string { return c.name }
14 func (c *counter) With(metrics.Field) metrics.Counter { return c }
15 func (c *counter) Add(delta uint64) {}
10 // With implements Counter.
11 func (c counter) With(labelValues ...string) metrics.Counter { return c }
1612
17 type gauge struct {
18 name string
19 }
13 // Add implements Counter.
14 func (c counter) Add(delta float64) {}
2015
21 // NewGauge returns a Gauge that does nothing.
22 func NewGauge(name string) metrics.Gauge { return &gauge{name} }
16 type gauge struct{}
2317
24 func (g *gauge) Name() string { return g.name }
25 func (g *gauge) With(metrics.Field) metrics.Gauge { return g }
26 func (g *gauge) Set(value float64) {}
27 func (g *gauge) Add(delta float64) {}
28 func (g *gauge) Get() float64 { return 0 }
18 // NewGauge returns a new no-op gauge.
19 func NewGauge() metrics.Gauge { return gauge{} }
2920
30 type histogram struct {
31 name string
32 }
21 // With implements Gauge.
22 func (g gauge) With(labelValues ...string) metrics.Gauge { return g }
3323
34 // NewHistogram returns a Histogram that does nothing.
35 func NewHistogram(name string) metrics.Histogram { return &histogram{name} }
24 // Set implements Gauge.
25 func (g gauge) Set(value float64) {}
3626
37 func (h *histogram) Name() string { return h.name }
38 func (h *histogram) With(metrics.Field) metrics.Histogram { return h }
39 func (h *histogram) Observe(value int64) {}
40 func (h *histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
41 return []metrics.Bucket{}, []metrics.Quantile{}
42 }
27 type histogram struct{}
28
29 // NewHistogram returns a new no-op histogram.
30 func NewHistogram() metrics.Histogram { return histogram{} }
31
32 // With implements Histogram.
33 func (h histogram) With(labelValues ...string) metrics.Histogram { return h }
34
35 // Observe implements histogram.
36 func (h histogram) Observe(value float64) {}
00 // Package metrics provides a framework for application instrumentation. All
11 // metrics are safe for concurrent use. Considerable design influence has been
22 // taken from https://github.com/codahale/metrics and https://prometheus.io.
3 //
4 // This package contains the common interfaces. Your code should take these
5 // interfaces as parameters. Implementations are provided for different
6 // instrumentation systems in the various subdirectories.
7 //
8 // Usage
9 //
10 // Metrics are dependencies and should be passed to the components that need
11 // them in the same way you'd construct and pass a database handle, or reference
12 // to another component. So, create metrics in your func main, using whichever
13 // concrete implementation is appropriate for your organization.
14 //
15 // latency := prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
16 // Namespace: "myteam",
17 // Subsystem: "foosvc",
18 // Name: "request_latency_seconds",
19 // Help: "Incoming request latency in seconds."
20 // }, []string{"method", "status_code"})
21 //
22 // Write your components to take the metrics they will use as parameters to
23 // their constructors. Use the interface types, not the concrete types. That is,
24 //
25 // // NewAPI takes metrics.Histogram, not *prometheus.Summary
26 // func NewAPI(s Store, logger log.Logger, latency metrics.Histogram) *API {
27 // // ...
28 // }
29 //
30 // func (a *API) ServeFoo(w http.ResponseWriter, r *http.Request) {
31 // begin := time.Now()
32 // // ...
33 // a.latency.Observe(time.Since(begin).Seconds())
34 // }
35 //
36 // Finally, pass the metrics as dependencies when building your object graph.
37 // This should happen in func main, not in the global scope.
38 //
39 // api := NewAPI(store, logger, latency)
40 // http.ListenAndServe("/", api)
41 //
42 // Implementation details
43 //
44 // Each telemetry system has different semantics for label values, push vs.
45 // pull, support for histograms, etc. These properties influence the design of
46 // their respective packages. This table attempts to summarize the key points of
47 // distinction.
48 //
49 // SYSTEM DIM COUNTERS GAUGES HISTOGRAMS
50 // dogstatsd n batch, push-aggregate batch, push-aggregate native, batch, push-each
51 // statsd 1 batch, push-aggregate batch, push-aggregate native, batch, push-each
52 // graphite 1 batch, push-aggregate batch, push-aggregate synthetic, batch, push-aggregate
53 // expvar 1 atomic atomic synthetic, batch, in-place expose
54 // influx n custom custom custom
55 // prometheus n native native native
56 // circonus 1 native native native
57 //
358 package metrics
0 // Package dogstatsd implements a DogStatsD backend for package metrics.
0 // Package dogstatsd provides a DogStatsD backend for package metrics. It's very
1 // similar to StatsD, but supports arbitrary tags per-metric, which map to Go
2 // kit's label values. So, while label values are no-ops in StatsD, they are
3 // supported here. For more details, see the documentation at
4 // http://docs.datadoghq.com/guides/dogstatsd/.
15 //
2 // This implementation supports Datadog tags that provide additional metric
3 // filtering capabilities. See the DogStatsD documentation for protocol
4 // specifics:
5 // http://docs.datadoghq.com/guides/dogstatsd/
6 //
6 // This package batches observations and emits them on some schedule to the
7 // remote server. This is useful even if you connect to your DogStatsD server
8 // over UDP. Emitting one network packet per observation can quickly overwhelm
9 // even the fastest internal network.
710 package dogstatsd
811
912 import (
10 "bytes"
1113 "fmt"
1214 "io"
13 "log"
14 "math"
15 "strings"
1516 "time"
1617
17 "sync/atomic"
18
19 "github.com/go-kit/kit/metrics"
18 "github.com/go-kit/kit/log"
19 "github.com/go-kit/kit/metrics3"
20 "github.com/go-kit/kit/metrics3/internal/lv"
21 "github.com/go-kit/kit/metrics3/internal/ratemap"
22 "github.com/go-kit/kit/util/conn"
2023 )
2124
22 // dogstatsd metrics were based on the statsd package in go-kit
23
24 const maxBufferSize = 1400 // bytes
25
26 type counter struct {
27 key string
28 c chan string
29 tags []metrics.Field
30 }
31
32 // NewCounter returns a Counter that emits observations in the DogStatsD protocol
33 // to the passed writer. Observations are buffered for the report interval or
34 // until the buffer exceeds a max packet size, whichever comes first.
25 // Dogstatsd receives metrics observations and forwards them to a DogStatsD
26 // server. Create a Dogstatsd object, use it to create metrics, and pass those
27 // metrics as dependencies to the components that will use them.
3528 //
36 // TODO: support for sampling.
37 func NewCounter(w io.Writer, key string, reportInterval time.Duration, globalTags []metrics.Field) metrics.Counter {
38 return NewCounterTick(w, key, time.Tick(reportInterval), globalTags)
39 }
40
41 // NewCounterTick is the same as NewCounter, but allows the user to pass in a
42 // ticker channel instead of invoking time.Tick.
43 func NewCounterTick(w io.Writer, key string, reportTicker <-chan time.Time, tags []metrics.Field) metrics.Counter {
44 c := &counter{
45 key: key,
46 c: make(chan string),
47 tags: tags,
48 }
49 go fwd(w, key, reportTicker, c.c)
50 return c
51 }
52
53 func (c *counter) Name() string { return c.key }
54
55 func (c *counter) With(f metrics.Field) metrics.Counter {
56 return &counter{
57 key: c.key,
58 c: c.c,
59 tags: append(c.tags, f),
60 }
61 }
62
63 func (c *counter) Add(delta uint64) { c.c <- applyTags(fmt.Sprintf("%d|c", delta), c.tags) }
64
65 type gauge struct {
66 key string
67 lastValue uint64 // math.Float64frombits
68 g chan string
69 tags []metrics.Field
70 }
71
72 // NewGauge returns a Gauge that emits values in the DogStatsD protocol to the
73 // passed writer. Values are buffered for the report interval or until the
74 // buffer exceeds a max packet size, whichever comes first.
29 // All metrics are buffered until WriteTo is called. Counters and gauges are
30 // aggregated into a single observation per timeseries per write. Timings and
31 // histograms are buffered but not aggregated.
7532 //
76 // TODO: support for sampling.
77 func NewGauge(w io.Writer, key string, reportInterval time.Duration, tags []metrics.Field) metrics.Gauge {
78 return NewGaugeTick(w, key, time.Tick(reportInterval), tags)
79 }
80
81 // NewGaugeTick is the same as NewGauge, but allows the user to pass in a ticker
82 // channel instead of invoking time.Tick.
83 func NewGaugeTick(w io.Writer, key string, reportTicker <-chan time.Time, tags []metrics.Field) metrics.Gauge {
84 g := &gauge{
85 key: key,
86 g: make(chan string),
87 tags: tags,
88 }
89 go fwd(w, key, reportTicker, g.g)
90 return g
91 }
92
93 func (g *gauge) Name() string { return g.key }
94
95 func (g *gauge) With(f metrics.Field) metrics.Gauge {
96 return &gauge{
97 key: g.key,
98 lastValue: g.lastValue,
99 g: g.g,
100 tags: append(g.tags, f),
101 }
102 }
103
104 func (g *gauge) Add(delta float64) {
105 // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges
106 sign := "+"
107 if delta < 0 {
108 sign, delta = "-", -delta
109 }
110 g.g <- applyTags(fmt.Sprintf("%s%f|g", sign, delta), g.tags)
111 }
112
113 func (g *gauge) Set(value float64) {
114 atomic.StoreUint64(&g.lastValue, math.Float64bits(value))
115 g.g <- applyTags(fmt.Sprintf("%f|g", value), g.tags)
116 }
117
118 func (g *gauge) Get() float64 {
119 return math.Float64frombits(atomic.LoadUint64(&g.lastValue))
120 }
121
122 // NewCallbackGauge emits values in the DogStatsD protocol to the passed writer.
123 // It collects values every scrape interval from the callback. Values are
124 // buffered for the report interval or until the buffer exceeds a max packet
125 // size, whichever comes first. The report and scrape intervals may be the
126 // same. The callback determines the value, and fields are ignored, so
127 // NewCallbackGauge returns nothing.
128 func NewCallbackGauge(w io.Writer, key string, reportInterval, scrapeInterval time.Duration, callback func() float64) {
129 NewCallbackGaugeTick(w, key, time.Tick(reportInterval), time.Tick(scrapeInterval), callback)
130 }
131
132 // NewCallbackGaugeTick is the same as NewCallbackGauge, but allows the user to
133 // pass in ticker channels instead of durations to control report and scrape
134 // intervals.
135 func NewCallbackGaugeTick(w io.Writer, key string, reportTicker, scrapeTicker <-chan time.Time, callback func() float64) {
136 go fwd(w, key, reportTicker, emitEvery(scrapeTicker, callback))
137 }
138
139 func emitEvery(emitTicker <-chan time.Time, callback func() float64) <-chan string {
140 c := make(chan string)
141 go func() {
142 for range emitTicker {
143 c <- fmt.Sprintf("%f|g", callback())
144 }
145 }()
146 return c
147 }
148
149 type histogram struct {
150 key string
151 h chan string
152 tags []metrics.Field
153 }
154
155 // NewHistogram returns a Histogram that emits observations in the DogStatsD
156 // protocol to the passed writer. Observations are buffered for the reporting
157 // interval or until the buffer exceeds a max packet size, whichever comes
158 // first.
159 //
160 // NewHistogram is mapped to a statsd Timing, so observations should represent
161 // milliseconds. If you observe in units of nanoseconds, you can make the
162 // translation with a ScaledHistogram:
163 //
164 // NewScaledHistogram(dogstatsdHistogram, time.Millisecond)
165 //
166 // You can also enforce the constraint in a typesafe way with a millisecond
167 // TimeHistogram:
168 //
169 // NewTimeHistogram(dogstatsdHistogram, time.Millisecond)
170 //
171 // TODO: support for sampling.
172 func NewHistogram(w io.Writer, key string, reportInterval time.Duration, tags []metrics.Field) metrics.Histogram {
173 return NewHistogramTick(w, key, time.Tick(reportInterval), tags)
174 }
175
176 // NewHistogramTick is the same as NewHistogram, but allows the user to pass a
177 // ticker channel instead of invoking time.Tick.
178 func NewHistogramTick(w io.Writer, key string, reportTicker <-chan time.Time, tags []metrics.Field) metrics.Histogram {
179 h := &histogram{
180 key: key,
181 h: make(chan string),
182 tags: tags,
183 }
184 go fwd(w, key, reportTicker, h.h)
185 return h
186 }
187
188 func (h *histogram) Name() string { return h.key }
189
190 func (h *histogram) With(f metrics.Field) metrics.Histogram {
191 return &histogram{
192 key: h.key,
193 h: h.h,
194 tags: append(h.tags, f),
195 }
196 }
197
198 func (h *histogram) Observe(value int64) {
199 h.h <- applyTags(fmt.Sprintf("%d|ms", value), h.tags)
200 }
201
202 func (h *histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
203 // TODO(pb): no way to do this without introducing e.g. codahale/hdrhistogram
204 return []metrics.Bucket{}, []metrics.Quantile{}
205 }
206
207 func fwd(w io.Writer, key string, reportTicker <-chan time.Time, c <-chan string) {
208 buf := &bytes.Buffer{}
209 for {
210 select {
211 case s := <-c:
212 fmt.Fprintf(buf, "%s:%s\n", key, s)
213 if buf.Len() > maxBufferSize {
214 flush(w, buf)
33 // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
34 // To send to a DogStatsD server, use the SendLoop helper method.
35 type Dogstatsd struct {
36 prefix string
37 rates *ratemap.RateMap
38 counters *lv.Space
39 gauges *lv.Space
40 timings *lv.Space
41 histograms *lv.Space
42 logger log.Logger
43 }
44
45 // New returns a Dogstatsd object that may be used to create metrics. Prefix is
46 // applied to all created metrics. Callers must ensure that regular calls to
47 // WriteTo are performed, either manually or with one of the helper methods.
48 func New(prefix string, logger log.Logger) *Dogstatsd {
49 return &Dogstatsd{
50 prefix: prefix,
51 rates: ratemap.New(),
52 counters: lv.NewSpace(),
53 gauges: lv.NewSpace(),
54 timings: lv.NewSpace(),
55 histograms: lv.NewSpace(),
56 logger: logger,
57 }
58 }
59
60 // NewCounter returns a counter, sending observations to this Dogstatsd object.
61 func (d *Dogstatsd) NewCounter(name string, sampleRate float64) *Counter {
62 d.rates.Set(d.prefix+name, sampleRate)
63 return &Counter{
64 name: d.prefix + name,
65 obs: d.counters.Observe,
66 }
67 }
68
69 // NewGauge returns a gauge, sending observations to this Dogstatsd object.
70 func (d *Dogstatsd) NewGauge(name string) *Gauge {
71 return &Gauge{
72 name: d.prefix + name,
73 obs: d.gauges.Observe,
74 }
75 }
76
77 // NewTiming returns a histogram whose observations are interpreted as
78 // millisecond durations, and are forwarded to this Dogstatsd object.
79 func (d *Dogstatsd) NewTiming(name string, sampleRate float64) *Timing {
80 d.rates.Set(d.prefix+name, sampleRate)
81 return &Timing{
82 name: d.prefix + name,
83 obs: d.timings.Observe,
84 }
85 }
86
87 // NewHistogram returns a histogram whose observations are of an unspecified
88 // unit, and are forwarded to this Dogstatsd object.
89 func (d *Dogstatsd) NewHistogram(name string, sampleRate float64) *Histogram {
90 d.rates.Set(d.prefix+name, sampleRate)
91 return &Histogram{
92 name: d.prefix + name,
93 obs: d.histograms.Observe,
94 }
95 }
96
97 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
98 // time the passed channel fires. This method blocks until the channel is
99 // closed, so clients probably want to run it in its own goroutine. For typical
100 // usage, create a time.Ticker and pass its C channel to this method.
101 func (d *Dogstatsd) WriteLoop(c <-chan time.Time, w io.Writer) {
102 for range c {
103 if _, err := d.WriteTo(w); err != nil {
104 d.logger.Log("during", "WriteTo", "err", err)
105 }
106 }
107 }
108
109 // SendLoop is a helper method that wraps WriteLoop, passing a managed
110 // connection to the network and address. Like WriteLoop, this method blocks
111 // until the channel is closed, so clients probably want to start it in its own
112 // goroutine. For typical usage, create a time.Ticker and pass its C channel to
113 // this method.
114 func (d *Dogstatsd) SendLoop(c <-chan time.Time, network, address string) {
115 d.WriteLoop(c, conn.NewDefaultManager(network, address, d.logger))
116 }
117
118 // WriteTo flushes the buffered content of the metrics to the writer, in
119 // DogStatsD format. WriteTo abides best-effort semantics, so observations are
120 // lost if there is a problem with the write. Clients should be sure to call
121 // WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
122 func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) {
123 var n int
124
125 d.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
126 n, err = fmt.Fprintf(w, "%s:%f|c%s%s\n", name, sum(values), sampling(d.rates.Get(name)), tagValues(lvs))
127 if err != nil {
128 return false
129 }
130 count += int64(n)
131 return true
132 })
133 if err != nil {
134 return count, err
135 }
136
137 d.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
138 n, err = fmt.Fprintf(w, "%s:%f|g%s\n", name, last(values), tagValues(lvs))
139 if err != nil {
140 return false
141 }
142 count += int64(n)
143 return true
144 })
145 if err != nil {
146 return count, err
147 }
148
149 d.timings.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
150 sampleRate := d.rates.Get(name)
151 for _, value := range values {
152 n, err = fmt.Fprintf(w, "%s:%f|ms%s%s\n", name, value, sampling(sampleRate), tagValues(lvs))
153 if err != nil {
154 return false
215155 }
216
217 case <-reportTicker:
218 flush(w, buf)
219 }
220 }
221 }
222
223 func flush(w io.Writer, buf *bytes.Buffer) {
224 if buf.Len() <= 0 {
225 return
226 }
227 if _, err := w.Write(buf.Bytes()); err != nil {
228 log.Printf("error: could not write to dogstatsd: %v", err)
229 }
230 buf.Reset()
231 }
232
233 func applyTags(value string, tags []metrics.Field) string {
234 if len(tags) > 0 {
235 var tagsString string
236 for _, t := range tags {
237 switch tagsString {
238 case "":
239 tagsString = t.Key + ":" + t.Value
240 default:
241 tagsString = tagsString + "," + t.Key + ":" + t.Value
156 count += int64(n)
157 }
158 return true
159 })
160 if err != nil {
161 return count, err
162 }
163
164 d.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
165 sampleRate := d.rates.Get(name)
166 for _, value := range values {
167 n, err = fmt.Fprintf(w, "%s:%f|h%s%s\n", name, value, sampling(sampleRate), tagValues(lvs))
168 if err != nil {
169 return false
242170 }
243 }
244 value = value + "|#" + tagsString
245 }
246 return value
247 }
171 count += int64(n)
172 }
173 return true
174 })
175 if err != nil {
176 return count, err
177 }
178
179 return count, err
180 }
181
182 func sum(a []float64) float64 {
183 var v float64
184 for _, f := range a {
185 v += f
186 }
187 return v
188 }
189
190 func last(a []float64) float64 {
191 return a[len(a)-1]
192 }
193
194 func sampling(r float64) string {
195 var sv string
196 if r < 1.0 {
197 sv = fmt.Sprintf("|@%f", r)
198 }
199 return sv
200 }
201
202 func tagValues(labelValues []string) string {
203 if len(labelValues) == 0 {
204 return ""
205 }
206 if len(labelValues)%2 != 0 {
207 panic("tagValues received a labelValues with an odd number of strings")
208 }
209 pairs := make([]string, 0, len(labelValues)/2)
210 for i := 0; i < len(labelValues); i += 2 {
211 pairs = append(pairs, labelValues[i]+":"+labelValues[i+1])
212 }
213 return "|#" + strings.Join(pairs, ",")
214 }
215
216 type observeFunc func(name string, lvs lv.LabelValues, value float64)
217
218 // Counter is a DogStatsD counter. Observations are forwarded to a Dogstatsd
219 // object, and aggregated (summed) per timeseries.
220 type Counter struct {
221 name string
222 lvs lv.LabelValues
223 obs observeFunc
224 }
225
226 // With implements metrics.Counter.
227 func (c *Counter) With(labelValues ...string) metrics.Counter {
228 return &Counter{
229 name: c.name,
230 lvs: c.lvs.With(labelValues...),
231 obs: c.obs,
232 }
233 }
234
235 // Add implements metrics.Counter.
236 func (c *Counter) Add(delta float64) {
237 c.obs(c.name, c.lvs, delta)
238 }
239
240 // Gauge is a DogStatsD gauge. Observations are forwarded to a Dogstatsd
241 // object, and aggregated (the last observation selected) per timeseries.
242 type Gauge struct {
243 name string
244 lvs lv.LabelValues
245 obs observeFunc
246 }
247
248 // With implements metrics.Gauge.
249 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
250 return &Gauge{
251 name: g.name,
252 lvs: g.lvs.With(labelValues...),
253 obs: g.obs,
254 }
255 }
256
257 // Set implements metrics.Gauge.
258 func (g *Gauge) Set(value float64) {
259 g.obs(g.name, g.lvs, value)
260 }
261
262 // Timing is a DogStatsD timing, or metrics.Histogram. Observations are
263 // forwarded to a Dogstatsd object, and collected (but not aggregated) per
264 // timeseries.
265 type Timing struct {
266 name string
267 lvs lv.LabelValues
268 obs observeFunc
269 }
270
271 // With implements metrics.Timing.
272 func (t *Timing) With(labelValues ...string) metrics.Histogram {
273 return &Timing{
274 name: t.name,
275 lvs: t.lvs.With(labelValues...),
276 obs: t.obs,
277 }
278 }
279
280 // Observe implements metrics.Histogram. Value is interpreted as milliseconds.
281 func (t *Timing) Observe(value float64) {
282 t.obs(t.name, t.lvs, value)
283 }
284
285 // Histogram is a DogStatsD histrogram. Observations are forwarded to a
286 // Dogstatsd object, and collected (but not aggregated) per timeseries.
287 type Histogram struct {
288 name string
289 lvs lv.LabelValues
290 obs observeFunc
291 }
292
293 // With implements metrics.Histogram.
294 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
295 return &Histogram{
296 name: h.name,
297 lvs: h.lvs.With(labelValues...),
298 obs: h.obs,
299 }
300 }
301
302 // Observe implements metrics.Histogram.
303 func (h *Histogram) Observe(value float64) {
304 h.obs(h.name, h.lvs, value)
305 }
00 package dogstatsd
11
22 import (
3 "bytes"
4 "fmt"
5 "net"
6 "strings"
7 "sync"
83 "testing"
9 "time"
104
115 "github.com/go-kit/kit/log"
12 "github.com/go-kit/kit/metrics"
13 "github.com/go-kit/kit/util/conn"
6 "github.com/go-kit/kit/metrics3/teststat"
147 )
158
16 func TestEmitterCounter(t *testing.T) {
17 e, buf := testEmitter()
18
19 c := e.NewCounter("test_statsd_counter")
20 c.Add(1)
21 c.Add(2)
22
23 // give time for things to emit
24 time.Sleep(time.Millisecond * 250)
25 // force a flush and stop
26 e.Stop()
27
28 want := "prefix.test_statsd_counter:1|c\nprefix.test_statsd_counter:2|c\n"
29 have := buf.String()
30 if want != have {
31 t.Errorf("want %q, have %q", want, have)
9 func TestCounter(t *testing.T) {
10 prefix, name := "abc.", "def"
11 label, value := "label", "value"
12 regex := `^` + prefix + name + `:([0-9\.]+)\|c\|#` + label + `:` + value + `$`
13 d := New(prefix, log.NewNopLogger())
14 counter := d.NewCounter(name, 1.0).With(label, value)
15 valuef := teststat.SumLines(d, regex)
16 if err := teststat.TestCounter(counter, valuef); err != nil {
17 t.Fatal(err)
3218 }
3319 }
3420
35 func TestEmitterGauge(t *testing.T) {
36 e, buf := testEmitter()
21 func TestCounterSampled(t *testing.T) {
22 // This will involve multiplying the observed sum by the inverse of the
23 // sample rate and checking against the expected value within some
24 // tolerance.
25 t.Skip("TODO")
26 }
3727
38 g := e.NewGauge("test_statsd_gauge")
39
40 delta := 1.0
41 g.Add(delta)
42
43 // give time for things to emit
44 time.Sleep(time.Millisecond * 250)
45 // force a flush and stop
46 e.Stop()
47
48 want := fmt.Sprintf("prefix.test_statsd_gauge:+%f|g\n", delta)
49 have := buf.String()
50 if want != have {
51 t.Errorf("want %q, have %q", want, have)
28 func TestGauge(t *testing.T) {
29 prefix, name := "ghi.", "jkl"
30 label, value := "xyz", "abc"
31 regex := `^` + prefix + name + `:([0-9\.]+)\|g\|#` + label + `:` + value + `$`
32 d := New(prefix, log.NewNopLogger())
33 gauge := d.NewGauge(name).With(label, value)
34 valuef := teststat.LastLine(d, regex)
35 if err := teststat.TestGauge(gauge, valuef); err != nil {
36 t.Fatal(err)
5237 }
5338 }
5439
55 func TestEmitterHistogram(t *testing.T) {
56 e, buf := testEmitter()
57 h := e.NewHistogram("test_statsd_histogram")
40 // DogStatsD histograms just emit all observations. So, we collect them into
41 // a generic histogram, and run the statistics test on that.
5842
59 h.Observe(123)
60
61 // give time for things to emit
62 time.Sleep(time.Millisecond * 250)
63 // force a flush and stop
64 e.Stop()
65
66 want := "prefix.test_statsd_histogram:123|ms\n"
67 have := buf.String()
68 if want != have {
69 t.Errorf("want %q, have %q", want, have)
43 func TestHistogram(t *testing.T) {
44 prefix, name := "dogstatsd.", "histogram_test"
45 label, value := "abc", "def"
46 regex := `^` + prefix + name + `:([0-9\.]+)\|h\|#` + label + `:` + value + `$`
47 d := New(prefix, log.NewNopLogger())
48 histogram := d.NewHistogram(name, 1.0).With(label, value)
49 quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
50 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
51 t.Fatal(err)
7052 }
7153 }
7254
73 func TestCounter(t *testing.T) {
74 buf := &syncbuf{buf: &bytes.Buffer{}}
75 reportc := make(chan time.Time)
76 tags := []metrics.Field{}
77 c := NewCounterTick(buf, "test_statsd_counter", reportc, tags)
78
79 c.Add(1)
80 c.With(metrics.Field{"foo", "bar"}).Add(2)
81 c.With(metrics.Field{"foo", "bar"}).With(metrics.Field{"abc", "123"}).Add(2)
82 c.Add(3)
83
84 want, have := "test_statsd_counter:1|c\ntest_statsd_counter:2|c|#foo:bar\ntest_statsd_counter:2|c|#foo:bar,abc:123\ntest_statsd_counter:3|c\n", ""
85 by(t, 100*time.Millisecond, func() bool {
86 have = buf.String()
87 return want == have
88 }, func() {
89 reportc <- time.Now()
90 }, fmt.Sprintf("want %q, have %q", want, have))
91 }
92
93 func TestGauge(t *testing.T) {
94 buf := &syncbuf{buf: &bytes.Buffer{}}
95 reportc := make(chan time.Time)
96 tags := []metrics.Field{}
97 g := NewGaugeTick(buf, "test_statsd_gauge", reportc, tags)
98
99 delta := 1.0
100 g.Add(delta)
101
102 want, have := fmt.Sprintf("test_statsd_gauge:+%f|g\n", delta), ""
103 by(t, 100*time.Millisecond, func() bool {
104 have = buf.String()
105 return want == have
106 }, func() {
107 reportc <- time.Now()
108 }, fmt.Sprintf("want %q, have %q", want, have))
109
110 buf.Reset()
111 delta = -2.0
112 g.With(metrics.Field{"foo", "bar"}).Add(delta)
113
114 want, have = fmt.Sprintf("test_statsd_gauge:%f|g|#foo:bar\n", delta), ""
115 by(t, 100*time.Millisecond, func() bool {
116 have = buf.String()
117 return want == have
118 }, func() {
119 reportc <- time.Now()
120 }, fmt.Sprintf("want %q, have %q", want, have))
121
122 buf.Reset()
123 value := 3.0
124 g.With(metrics.Field{"foo", "bar"}).With(metrics.Field{"abc", "123"}).Set(value)
125
126 want, have = fmt.Sprintf("test_statsd_gauge:%f|g|#foo:bar,abc:123\n", value), ""
127 by(t, 100*time.Millisecond, func() bool {
128 have = buf.String()
129 return want == have
130 }, func() {
131 reportc <- time.Now()
132 }, fmt.Sprintf("want %q, have %q", want, have))
133 }
134
135 func TestCallbackGauge(t *testing.T) {
136 buf := &syncbuf{buf: &bytes.Buffer{}}
137 reportc, scrapec := make(chan time.Time), make(chan time.Time)
138 value := 55.55
139 cb := func() float64 { return value }
140 NewCallbackGaugeTick(buf, "test_statsd_callback_gauge", reportc, scrapec, cb)
141
142 scrapec <- time.Now()
143 reportc <- time.Now()
144
145 // Travis is annoying
146 by(t, time.Second, func() bool {
147 return buf.String() != ""
148 }, func() {
149 reportc <- time.Now()
150 }, "buffer never got write+flush")
151
152 want, have := fmt.Sprintf("test_statsd_callback_gauge:%f|g\n", value), ""
153 by(t, 100*time.Millisecond, func() bool {
154 have = buf.String()
155 return strings.HasPrefix(have, want) // HasPrefix because we might get multiple writes
156 }, func() {
157 reportc <- time.Now()
158 }, fmt.Sprintf("want %q, have %q", want, have))
159 }
160
161 func TestHistogram(t *testing.T) {
162 buf := &syncbuf{buf: &bytes.Buffer{}}
163 reportc := make(chan time.Time)
164 tags := []metrics.Field{}
165 h := NewHistogramTick(buf, "test_statsd_histogram", reportc, tags)
166
167 h.Observe(123)
168 h.With(metrics.Field{"foo", "bar"}).Observe(456)
169
170 want, have := "test_statsd_histogram:123|ms\ntest_statsd_histogram:456|ms|#foo:bar\n", ""
171 by(t, 100*time.Millisecond, func() bool {
172 have = buf.String()
173 return want == have
174 }, func() {
175 reportc <- time.Now()
176 }, fmt.Sprintf("want %q, have %q", want, have))
177 }
178
179 func by(t *testing.T, d time.Duration, check func() bool, execute func(), msg string) {
180 deadline := time.Now().Add(d)
181 for !check() {
182 if time.Now().After(deadline) {
183 t.Fatal(msg)
184 }
185 execute()
55 func TestHistogramSampled(t *testing.T) {
56 prefix, name := "dogstatsd.", "sampled_histogram_test"
57 label, value := "foo", "bar"
58 regex := `^` + prefix + name + `:([0-9\.]+)\|h\|@0\.01[0]*\|#` + label + `:` + value + `$`
59 d := New(prefix, log.NewNopLogger())
60 histogram := d.NewHistogram(name, 0.01).With(label, value)
61 quantiles := teststat.Quantiles(d, regex, 50)
62 if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
63 t.Fatal(err)
18664 }
18765 }
18866
189 type syncbuf struct {
190 mtx sync.Mutex
191 buf *bytes.Buffer
192 }
193
194 func (s *syncbuf) Write(p []byte) (int, error) {
195 s.mtx.Lock()
196 defer s.mtx.Unlock()
197 return s.buf.Write(p)
198 }
199
200 func (s *syncbuf) String() string {
201 s.mtx.Lock()
202 defer s.mtx.Unlock()
203 return s.buf.String()
204 }
205
206 func (s *syncbuf) Reset() {
207 s.mtx.Lock()
208 defer s.mtx.Unlock()
209 s.buf.Reset()
210 }
211
212 func testEmitter() (*Emitter, *syncbuf) {
213 buf := &syncbuf{buf: &bytes.Buffer{}}
214 e := &Emitter{
215 prefix: "prefix.",
216 mgr: conn.NewManager(mockDialer(buf), "", "", time.After, log.NewNopLogger()),
217 logger: log.NewNopLogger(),
218 keyVals: make(chan keyVal),
219 quitc: make(chan chan struct{}),
220 }
221 go e.loop(time.Millisecond * 20)
222 return e, buf
223 }
224
225 func mockDialer(buf *syncbuf) conn.Dialer {
226 return func(net, addr string) (net.Conn, error) {
227 return &mockConn{buf}, nil
67 func TestTiming(t *testing.T) {
68 prefix, name := "dogstatsd.", "timing_test"
69 label, value := "wiggle", "bottom"
70 regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|#` + label + `:` + value + `$`
71 d := New(prefix, log.NewNopLogger())
72 histogram := d.NewTiming(name, 1.0).With(label, value)
73 quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
74 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
75 t.Fatal(err)
22876 }
22977 }
23078
231 type mockConn struct {
232 buf *syncbuf
79 func TestTimingSampled(t *testing.T) {
80 prefix, name := "dogstatsd.", "sampled_timing_test"
81 label, value := "internal", "external"
82 regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0.03[0]*\|#` + label + `:` + value + `$`
83 d := New(prefix, log.NewNopLogger())
84 histogram := d.NewTiming(name, 0.03).With(label, value)
85 quantiles := teststat.Quantiles(d, regex, 50)
86 if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
87 t.Fatal(err)
88 }
23389 }
234
235 func (c *mockConn) Read(b []byte) (n int, err error) {
236 panic("not implemented")
237 }
238
239 func (c *mockConn) Write(b []byte) (n int, err error) {
240 return c.buf.Write(b)
241 }
242
243 func (c *mockConn) Close() error {
244 panic("not implemented")
245 }
246
247 func (c *mockConn) LocalAddr() net.Addr {
248 panic("not implemented")
249 }
250
251 func (c *mockConn) RemoteAddr() net.Addr {
252 panic("not implemented")
253 }
254
255 func (c *mockConn) SetDeadline(t time.Time) error {
256 panic("not implemented")
257 }
258
259 func (c *mockConn) SetReadDeadline(t time.Time) error {
260 panic("not implemented")
261 }
262
263 func (c *mockConn) SetWriteDeadline(t time.Time) error {
264 panic("not implemented")
265 }
+0
-159
metrics/dogstatsd/emitter.go less more
0 package dogstatsd
1
2 import (
3 "bytes"
4 "fmt"
5 "net"
6 "time"
7
8 "github.com/go-kit/kit/log"
9 "github.com/go-kit/kit/metrics"
10 "github.com/go-kit/kit/util/conn"
11 )
12
13 // Emitter is a struct to manage connections and orchestrate the emission of
14 // metrics to a DogStatsd process.
15 type Emitter struct {
16 prefix string
17 keyVals chan keyVal
18 mgr *conn.Manager
19 logger log.Logger
20 quitc chan chan struct{}
21 }
22
23 type keyVal struct {
24 key string
25 val string
26 }
27
28 func stringToKeyVal(key string, keyVals chan keyVal) chan string {
29 vals := make(chan string)
30 go func() {
31 for val := range vals {
32 keyVals <- keyVal{key: key, val: val}
33 }
34 }()
35 return vals
36 }
37
38 // NewEmitter will return an Emitter that will prefix all metrics names with the
39 // given prefix. Once started, it will attempt to create a connection with the
40 // given network and address via `net.Dial` and periodically post metrics to the
41 // connection in the DogStatsD protocol.
42 func NewEmitter(network, address string, metricsPrefix string, flushInterval time.Duration, logger log.Logger) *Emitter {
43 return NewEmitterDial(net.Dial, network, address, metricsPrefix, flushInterval, logger)
44 }
45
46 // NewEmitterDial is the same as NewEmitter, but allows you to specify your own
47 // Dialer function. This is primarily useful for tests.
48 func NewEmitterDial(dialer conn.Dialer, network, address string, metricsPrefix string, flushInterval time.Duration, logger log.Logger) *Emitter {
49 e := &Emitter{
50 prefix: metricsPrefix,
51 mgr: conn.NewManager(dialer, network, address, time.After, logger),
52 logger: logger,
53 keyVals: make(chan keyVal),
54 quitc: make(chan chan struct{}),
55 }
56 go e.loop(flushInterval)
57 return e
58 }
59
60 // NewCounter returns a Counter that emits observations in the DogStatsD protocol
61 // via the Emitter's connection manager. Observations are buffered for the
62 // report interval or until the buffer exceeds a max packet size, whichever
63 // comes first. Fields are ignored.
64 func (e *Emitter) NewCounter(key string) metrics.Counter {
65 key = e.prefix + key
66 return &counter{
67 key: key,
68 c: stringToKeyVal(key, e.keyVals),
69 }
70 }
71
72 // NewHistogram returns a Histogram that emits observations in the DogStatsD
73 // protocol via the Emitter's connection manager. Observations are buffered for
74 // the reporting interval or until the buffer exceeds a max packet size,
75 // whichever comes first. Fields are ignored.
76 //
77 // NewHistogram is mapped to a statsd Timing, so observations should represent
78 // milliseconds. If you observe in units of nanoseconds, you can make the
79 // translation with a ScaledHistogram:
80 //
81 // NewScaledHistogram(histogram, time.Millisecond)
82 //
83 // You can also enforce the constraint in a typesafe way with a millisecond
84 // TimeHistogram:
85 //
86 // NewTimeHistogram(histogram, time.Millisecond)
87 //
88 // TODO: support for sampling.
89 func (e *Emitter) NewHistogram(key string) metrics.Histogram {
90 key = e.prefix + key
91 return &histogram{
92 key: key,
93 h: stringToKeyVal(key, e.keyVals),
94 }
95 }
96
97 // NewGauge returns a Gauge that emits values in the DogStatsD protocol via the
98 // the Emitter's connection manager. Values are buffered for the report
99 // interval or until the buffer exceeds a max packet size, whichever comes
100 // first. Fields are ignored.
101 //
102 // TODO: support for sampling
103 func (e *Emitter) NewGauge(key string) metrics.Gauge {
104 key = e.prefix + key
105 return &gauge{
106 key: key,
107 g: stringToKeyVal(key, e.keyVals),
108 }
109 }
110
111 func (e *Emitter) loop(d time.Duration) {
112 ticker := time.NewTicker(d)
113 defer ticker.Stop()
114 buf := &bytes.Buffer{}
115 for {
116 select {
117 case kv := <-e.keyVals:
118 fmt.Fprintf(buf, "%s:%s\n", kv.key, kv.val)
119 if buf.Len() > maxBufferSize {
120 e.Flush(buf)
121 }
122
123 case <-ticker.C:
124 e.Flush(buf)
125
126 case q := <-e.quitc:
127 e.Flush(buf)
128 close(q)
129 return
130 }
131 }
132 }
133
134 // Stop will flush the current metrics and close the active connection. Calling
135 // stop more than once is a programmer error.
136 func (e *Emitter) Stop() {
137 q := make(chan struct{})
138 e.quitc <- q
139 <-q
140 }
141
142 // Flush will write the given buffer to a connection provided by the Emitter's
143 // connection manager.
144 func (e *Emitter) Flush(buf *bytes.Buffer) {
145 conn := e.mgr.Take()
146 if conn == nil {
147 e.logger.Log("during", "flush", "err", "connection unavailable")
148 return
149 }
150
151 _, err := conn.Write(buf.Bytes())
152 if err != nil {
153 e.logger.Log("during", "flush", "err", err)
154 }
155 buf.Reset()
156
157 e.mgr.Put(err)
158 }
0 // Package expvar implements an expvar backend for package metrics.
1 //
2 // The current implementation ignores fields. In the future, it would be good
3 // to have an implementation that accepted a set of predeclared field names at
4 // construction time, and used field values to produce delimiter-separated
5 // bucket (key) names. That is,
6 //
7 // c := NewFieldedCounter(..., "path", "status")
8 // c.Add(1) // "myprefix_unknown_unknown" += 1
9 // c2 := c.With("path", "foo").With("status": "200")
10 // c2.Add(1) // "myprefix_foo_200" += 1
11 //
12 // It would also be possible to have an implementation that generated more
13 // sophisticated expvar.Values. For example, a Counter could be implemented as
14 // a map, representing a tree of key/value pairs whose leaves were the actual
15 // expvar.Ints.
0 // Package expvar provides expvar backends for metrics.
1 // Label values are not supported.
162 package expvar
173
184 import (
195 "expvar"
20 "fmt"
21 "sort"
22 "strconv"
236 "sync"
24 "time"
257
26 "github.com/codahale/hdrhistogram"
27
28 "github.com/go-kit/kit/metrics"
8 "github.com/go-kit/kit/metrics3"
9 "github.com/go-kit/kit/metrics3/generic"
2910 )
3011
31 type counter struct {
32 name string
33 v *expvar.Int
12 // Counter implements the counter metric with an expvar float.
13 // Label values are not supported.
14 type Counter struct {
15 f *expvar.Float
3416 }
3517
36 // NewCounter returns a new Counter backed by an expvar with the given name.
37 // Fields are ignored.
38 func NewCounter(name string) metrics.Counter {
39 return &counter{
40 name: name,
41 v: expvar.NewInt(name),
18 // NewCounter creates an expvar Float with the given name, and returns an object
19 // that implements the Counter interface.
20 func NewCounter(name string) *Counter {
21 return &Counter{
22 f: expvar.NewFloat(name),
4223 }
4324 }
4425
45 func (c *counter) Name() string { return c.name }
46 func (c *counter) With(metrics.Field) metrics.Counter { return c }
47 func (c *counter) Add(delta uint64) { c.v.Add(int64(delta)) }
26 // With is a no-op.
27 func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
4828
49 type gauge struct {
50 name string
51 v *expvar.Float
29 // Add implements Counter.
30 func (c *Counter) Add(delta float64) { c.f.Add(delta) }
31
32 // Gauge implements the gauge metric wtih an expvar float.
33 // Label values are not supported.
34 type Gauge struct {
35 f *expvar.Float
5236 }
5337
54 // NewGauge returns a new Gauge backed by an expvar with the given name. It
55 // should be updated manually; for a callback-based approach, see
56 // PublishCallbackGauge. Fields are ignored.
57 func NewGauge(name string) metrics.Gauge {
58 return &gauge{
59 name: name,
60 v: expvar.NewFloat(name),
38 // NewGauge creates an expvar Float with the given name, and returns an object
39 // that implements the Gauge interface.
40 func NewGauge(name string) *Gauge {
41 return &Gauge{
42 f: expvar.NewFloat(name),
6143 }
6244 }
6345
64 func (g *gauge) Name() string { return g.name }
65 func (g *gauge) With(metrics.Field) metrics.Gauge { return g }
66 func (g *gauge) Add(delta float64) { g.v.Add(delta) }
67 func (g *gauge) Set(value float64) { g.v.Set(value) }
68 func (g *gauge) Get() float64 { return mustParseFloat64(g.v.String()) }
46 // With is a no-op.
47 func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
6948
70 // PublishCallbackGauge publishes a Gauge as an expvar with the given name,
71 // whose value is determined at collect time by the passed callback function.
72 // The callback determines the value, and fields are ignored, so
73 // PublishCallbackGauge returns nothing.
74 func PublishCallbackGauge(name string, callback func() float64) {
75 expvar.Publish(name, callbackGauge(callback))
49 // Set implements Gauge.
50 func (g *Gauge) Set(value float64) { g.f.Set(value) }
51
52 // Histogram implements the histogram metric with a combination of the generic
53 // Histogram object and several expvar Floats, one for each of the 50th, 90th,
54 // 95th, and 99th quantiles of observed values, with the quantile attached to
55 // the name as a suffix. Label values are not supported.
56 type Histogram struct {
57 mtx sync.Mutex
58 h *generic.Histogram
59 p50 *expvar.Float
60 p90 *expvar.Float
61 p95 *expvar.Float
62 p99 *expvar.Float
7663 }
7764
78 type callbackGauge func() float64
79
80 func (g callbackGauge) String() string { return strconv.FormatFloat(g(), 'g', -1, 64) }
81
82 type histogram struct {
83 mu sync.Mutex
84 hist *hdrhistogram.WindowedHistogram
85
86 name string
87 gauges map[int]metrics.Gauge
88 }
89
90 // NewHistogram is taken from http://github.com/codahale/metrics. It returns a
91 // windowed HDR histogram which drops data older than five minutes.
92 //
93 // The histogram exposes metrics for each passed quantile as gauges. Quantiles
94 // should be integers in the range 1..99. The gauge names are assigned by
95 // using the passed name as a prefix and appending "_pNN" e.g. "_p50".
96 func NewHistogram(name string, minValue, maxValue int64, sigfigs int, quantiles ...int) metrics.Histogram {
97 gauges := map[int]metrics.Gauge{}
98 for _, quantile := range quantiles {
99 if quantile <= 0 || quantile >= 100 {
100 panic(fmt.Sprintf("invalid quantile %d", quantile))
101 }
102 gauges[quantile] = NewGauge(fmt.Sprintf("%s_p%02d", name, quantile))
103 }
104 h := &histogram{
105 hist: hdrhistogram.NewWindowed(5, minValue, maxValue, sigfigs),
106 name: name,
107 gauges: gauges,
108 }
109 go h.rotateLoop(1 * time.Minute)
110 return h
111 }
112
113 func (h *histogram) Name() string { return h.name }
114 func (h *histogram) With(metrics.Field) metrics.Histogram { return h }
115
116 func (h *histogram) Observe(value int64) {
117 h.mu.Lock()
118 err := h.hist.Current.RecordValue(value)
119 h.mu.Unlock()
120
121 if err != nil {
122 panic(err.Error())
123 }
124
125 for q, gauge := range h.gauges {
126 gauge.Set(float64(h.hist.Current.ValueAtQuantile(float64(q))))
65 // NewHistogram returns a Histogram object with the given name and number of
66 // buckets in the underlying histogram object. 50 is a good default number of
67 // buckets.
68 func NewHistogram(name string, buckets int) *Histogram {
69 return &Histogram{
70 h: generic.NewHistogram(name, buckets),
71 p50: expvar.NewFloat(name + ".p50"),
72 p90: expvar.NewFloat(name + ".p90"),
73 p95: expvar.NewFloat(name + ".p95"),
74 p99: expvar.NewFloat(name + ".p99"),
12775 }
12876 }
12977
130 func (h *histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
131 bars := h.hist.Merge().Distribution()
132 buckets := make([]metrics.Bucket, len(bars))
133 for i, bar := range bars {
134 buckets[i] = metrics.Bucket{
135 From: bar.From,
136 To: bar.To,
137 Count: bar.Count,
138 }
139 }
140 quantiles := make([]metrics.Quantile, 0, len(h.gauges))
141 for quantile, gauge := range h.gauges {
142 quantiles = append(quantiles, metrics.Quantile{
143 Quantile: quantile,
144 Value: int64(gauge.Get()),
145 })
146 }
147 sort.Sort(quantileSlice(quantiles))
148 return buckets, quantiles
78 // With is a no-op.
79 func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
80
81 // Observe impleemts Histogram.
82 func (h *Histogram) Observe(value float64) {
83 h.mtx.Lock()
84 defer h.mtx.Unlock()
85 h.h.Observe(value)
86 h.p50.Set(h.h.Quantile(0.50))
87 h.p90.Set(h.h.Quantile(0.90))
88 h.p95.Set(h.h.Quantile(0.95))
89 h.p99.Set(h.h.Quantile(0.99))
14990 }
150
151 func (h *histogram) rotateLoop(d time.Duration) {
152 for range time.Tick(d) {
153 h.mu.Lock()
154 h.hist.Rotate()
155 h.mu.Unlock()
156 }
157 }
158
159 func mustParseFloat64(s string) float64 {
160 f, err := strconv.ParseFloat(s, 64)
161 if err != nil {
162 panic(err)
163 }
164 return f
165 }
166
167 type quantileSlice []metrics.Quantile
168
169 func (a quantileSlice) Len() int { return len(a) }
170 func (a quantileSlice) Less(i, j int) bool { return a[i].Quantile < a[j].Quantile }
171 func (a quantileSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
0 package expvar_test
0 package expvar
11
22 import (
3 stdexpvar "expvar"
4 "fmt"
3 "strconv"
54 "testing"
65
7 "github.com/go-kit/kit/metrics"
8 "github.com/go-kit/kit/metrics/expvar"
9 "github.com/go-kit/kit/metrics/teststat"
6 "github.com/go-kit/kit/metrics3/teststat"
107 )
118
12 func TestHistogramQuantiles(t *testing.T) {
13 var (
14 name = "test_histogram_quantiles"
15 quantiles = []int{50, 90, 95, 99}
16 h = expvar.NewHistogram(name, 0, 100, 3, quantiles...).With(metrics.Field{Key: "ignored", Value: "field"})
17 )
18 const seed, mean, stdev int64 = 424242, 50, 10
19 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
20 teststat.AssertExpvarNormalHistogram(t, name, mean, stdev, quantiles)
21 }
22
23 func TestCallbackGauge(t *testing.T) {
24 var (
25 name = "foo"
26 value = 42.43
27 )
28 expvar.PublishCallbackGauge(name, func() float64 { return value })
29 if want, have := fmt.Sprint(value), stdexpvar.Get(name).String(); want != have {
30 t.Errorf("want %q, have %q", want, have)
31 }
32 }
33
349 func TestCounter(t *testing.T) {
35 var (
36 name = "m"
37 value = 123
38 )
39 expvar.NewCounter(name).With(metrics.Field{Key: "ignored", Value: "field"}).Add(uint64(value))
40 if want, have := fmt.Sprint(value), stdexpvar.Get(name).String(); want != have {
41 t.Errorf("want %q, have %q", want, have)
10 counter := NewCounter("expvar_counter").With("label values", "not supported").(*Counter)
11 value := func() float64 { f, _ := strconv.ParseFloat(counter.f.String(), 64); return f }
12 if err := teststat.TestCounter(counter, value); err != nil {
13 t.Fatal(err)
4214 }
4315 }
4416
4517 func TestGauge(t *testing.T) {
46 var (
47 name = "xyz"
48 value = 54321
49 delta = 12345
50 g = expvar.NewGauge(name).With(metrics.Field{Key: "ignored", Value: "field"})
51 )
52 g.Set(float64(value))
53 g.Add(float64(delta))
54 if want, have := fmt.Sprint(value+delta), stdexpvar.Get(name).String(); want != have {
55 t.Errorf("want %q, have %q", want, have)
18 gauge := NewGauge("expvar_gauge").With("label values", "not supported").(*Gauge)
19 value := func() float64 { f, _ := strconv.ParseFloat(gauge.f.String(), 64); return f }
20 if err := teststat.TestGauge(gauge, value); err != nil {
21 t.Fatal(err)
5622 }
5723 }
5824
59 func TestInvalidQuantile(t *testing.T) {
60 defer func() {
61 if err := recover(); err == nil {
62 t.Errorf("expected panic, got none")
63 } else {
64 t.Logf("got expected panic: %v", err)
65 }
66 }()
67 expvar.NewHistogram("foo", 0.0, 100.0, 3, 50, 90, 95, 99, 101)
25 func TestHistogram(t *testing.T) {
26 histogram := NewHistogram("expvar_histogram", 50).With("label values", "not supported").(*Histogram)
27 quantiles := func() (float64, float64, float64, float64) {
28 p50, _ := strconv.ParseFloat(histogram.p50.String(), 64)
29 p90, _ := strconv.ParseFloat(histogram.p90.String(), 64)
30 p95, _ := strconv.ParseFloat(histogram.p95.String(), 64)
31 p99, _ := strconv.ParseFloat(histogram.p99.String(), 64)
32 return p50, p90, p95, p99
33 }
34 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
35 t.Fatal(err)
36 }
6837 }
0 // Package generic implements generic versions of each of the metric types. They
1 // can be embedded by other implementations, and converted to specific formats
2 // as necessary.
3 package generic
4
5 import (
6 "fmt"
7 "io"
8 "math"
9 "sync"
10 "sync/atomic"
11
12 "github.com/VividCortex/gohistogram"
13
14 "github.com/go-kit/kit/metrics3"
15 "github.com/go-kit/kit/metrics3/internal/lv"
16 )
17
18 // Counter is an in-memory implementation of a Counter.
19 type Counter struct {
20 Name string
21 lvs lv.LabelValues
22 bits uint64
23 }
24
25 // NewCounter returns a new, usable Counter.
26 func NewCounter(name string) *Counter {
27 return &Counter{
28 Name: name,
29 }
30 }
31
32 // With implements Counter.
33 func (c *Counter) With(labelValues ...string) metrics.Counter {
34 return &Counter{
35 bits: atomic.LoadUint64(&c.bits),
36 lvs: c.lvs.With(labelValues...),
37 }
38 }
39
40 // Add implements Counter.
41 func (c *Counter) Add(delta float64) {
42 for {
43 var (
44 old = atomic.LoadUint64(&c.bits)
45 newf = math.Float64frombits(old) + delta
46 new = math.Float64bits(newf)
47 )
48 if atomic.CompareAndSwapUint64(&c.bits, old, new) {
49 break
50 }
51 }
52 }
53
54 // Value returns the current value of the counter.
55 func (c *Counter) Value() float64 {
56 return math.Float64frombits(atomic.LoadUint64(&c.bits))
57 }
58
59 // ValueReset returns the current value of the counter, and resets it to zero.
60 // This is useful for metrics backends whose counter aggregations expect deltas,
61 // like Graphite.
62 func (c *Counter) ValueReset() float64 {
63 for {
64 var (
65 old = atomic.LoadUint64(&c.bits)
66 newf = 0.0
67 new = math.Float64bits(newf)
68 )
69 if atomic.CompareAndSwapUint64(&c.bits, old, new) {
70 return math.Float64frombits(old)
71 }
72 }
73 }
74
75 // LabelValues returns the set of label values attached to the counter.
76 func (c *Counter) LabelValues() []string {
77 return c.lvs
78 }
79
80 // Gauge is an in-memory implementation of a Gauge.
81 type Gauge struct {
82 Name string
83 lvs lv.LabelValues
84 bits uint64
85 }
86
87 // NewGauge returns a new, usable Gauge.
88 func NewGauge(name string) *Gauge {
89 return &Gauge{
90 Name: name,
91 }
92 }
93
94 // With implements Gauge.
95 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
96 return &Gauge{
97 bits: atomic.LoadUint64(&g.bits),
98 lvs: g.lvs.With(labelValues...),
99 }
100 }
101
102 // Set implements Gauge.
103 func (g *Gauge) Set(value float64) {
104 atomic.StoreUint64(&g.bits, math.Float64bits(value))
105 }
106
107 // Value returns the current value of the gauge.
108 func (g *Gauge) Value() float64 {
109 return math.Float64frombits(atomic.LoadUint64(&g.bits))
110 }
111
112 // LabelValues returns the set of label values attached to the gauge.
113 func (g *Gauge) LabelValues() []string {
114 return g.lvs
115 }
116
117 // Histogram is an in-memory implementation of a streaming histogram, based on
118 // VividCortex/gohistogram. It dynamically computes quantiles, so it's not
119 // suitable for aggregation.
120 type Histogram struct {
121 Name string
122 lvs lv.LabelValues
123 h gohistogram.Histogram
124 }
125
126 // NewHistogram returns a numeric histogram based on VividCortex/gohistogram. A
127 // good default value for buckets is 50.
128 func NewHistogram(name string, buckets int) *Histogram {
129 return &Histogram{
130 Name: name,
131 h: gohistogram.NewHistogram(buckets),
132 }
133 }
134
135 // With implements Histogram.
136 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
137 return &Histogram{
138 lvs: h.lvs.With(labelValues...),
139 h: h.h,
140 }
141 }
142
143 // Observe implements Histogram.
144 func (h *Histogram) Observe(value float64) {
145 h.h.Add(value)
146 }
147
148 // Quantile returns the value of the quantile q, 0.0 < q < 1.0.
149 func (h *Histogram) Quantile(q float64) float64 {
150 return h.h.Quantile(q)
151 }
152
153 // LabelValues returns the set of label values attached to the histogram.
154 func (h *Histogram) LabelValues() []string {
155 return h.lvs
156 }
157
158 // Print writes a string representation of the histogram to the passed writer.
159 // Useful for printing to a terminal.
160 func (h *Histogram) Print(w io.Writer) {
161 fmt.Fprintf(w, h.h.String())
162 }
163
164 // Bucket is a range in a histogram which aggregates observations.
165 type Bucket struct {
166 From, To, Count int64
167 }
168
169 // Quantile is a pair of a quantile (0..100) and its observed maximum value.
170 type Quantile struct {
171 Quantile int // 0..100
172 Value int64
173 }
174
175 // SimpleHistogram is an in-memory implementation of a Histogram. It only tracks
176 // an approximate moving average, so is likely too naïve for many use cases.
177 type SimpleHistogram struct {
178 mtx sync.RWMutex
179 lvs lv.LabelValues
180 avg float64
181 n uint64
182 }
183
184 // NewSimpleHistogram returns a SimpleHistogram, ready for observations.
185 func NewSimpleHistogram() *SimpleHistogram {
186 return &SimpleHistogram{}
187 }
188
189 // With implements Histogram.
190 func (h *SimpleHistogram) With(labelValues ...string) metrics.Histogram {
191 return &SimpleHistogram{
192 lvs: h.lvs.With(labelValues...),
193 avg: h.avg,
194 n: h.n,
195 }
196 }
197
198 // Observe implements Histogram.
199 func (h *SimpleHistogram) Observe(value float64) {
200 h.mtx.Lock()
201 defer h.mtx.Unlock()
202 h.n++
203 h.avg -= h.avg / float64(h.n)
204 h.avg += value / float64(h.n)
205 }
206
207 // ApproximateMovingAverage returns the approximate moving average of observations.
208 func (h *SimpleHistogram) ApproximateMovingAverage() float64 {
209 h.mtx.RLock()
210 h.mtx.RUnlock()
211 return h.avg
212 }
213
214 // LabelValues returns the set of label values attached to the histogram.
215 func (h *SimpleHistogram) LabelValues() []string {
216 return h.lvs
217 }
0 package generic_test
1
2 // This is package generic_test in order to get around an import cycle: this
3 // package imports teststat to do its testing, but package teststat imports
4 // generic to use its Histogram in the Quantiles helper function.
5
6 import (
7 "math"
8 "math/rand"
9 "testing"
10
11 "github.com/go-kit/kit/metrics3/generic"
12 "github.com/go-kit/kit/metrics3/teststat"
13 )
14
15 func TestCounter(t *testing.T) {
16 counter := generic.NewCounter("my_counter").With("label", "counter").(*generic.Counter)
17 value := func() float64 { return counter.Value() }
18 if err := teststat.TestCounter(counter, value); err != nil {
19 t.Fatal(err)
20 }
21 }
22
23 func TestValueReset(t *testing.T) {
24 counter := generic.NewCounter("test_value_reset")
25 counter.Add(123)
26 counter.Add(456)
27 counter.Add(789)
28 if want, have := float64(123+456+789), counter.ValueReset(); want != have {
29 t.Errorf("want %f, have %f", want, have)
30 }
31 if want, have := float64(0), counter.Value(); want != have {
32 t.Errorf("want %f, have %f", want, have)
33 }
34 }
35
36 func TestGauge(t *testing.T) {
37 gauge := generic.NewGauge("my_gauge").With("label", "gauge").(*generic.Gauge)
38 value := func() float64 { return gauge.Value() }
39 if err := teststat.TestGauge(gauge, value); err != nil {
40 t.Fatal(err)
41 }
42 }
43
44 func TestHistogram(t *testing.T) {
45 histogram := generic.NewHistogram("my_histogram", 50).With("label", "histogram").(*generic.Histogram)
46 quantiles := func() (float64, float64, float64, float64) {
47 return histogram.Quantile(0.50), histogram.Quantile(0.90), histogram.Quantile(0.95), histogram.Quantile(0.99)
48 }
49 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
50 t.Fatal(err)
51 }
52 }
53
54 func TestSimpleHistogram(t *testing.T) {
55 histogram := generic.NewSimpleHistogram().With("label", "simple_histogram").(*generic.SimpleHistogram)
56 var (
57 sum int
58 count = 1234 // not too big
59 )
60 for i := 0; i < count; i++ {
61 value := rand.Intn(1000)
62 sum += value
63 histogram.Observe(float64(value))
64 }
65
66 var (
67 want = float64(sum) / float64(count)
68 have = histogram.ApproximateMovingAverage()
69 tolerance = 0.001 // real real slim
70 )
71 if math.Abs(want-have)/want > tolerance {
72 t.Errorf("want %f, have %f", want, have)
73 }
74 }
+0
-159
metrics/graphite/emitter.go less more
0 package graphite
1
2 import (
3 "bufio"
4 "fmt"
5 "io"
6 "net"
7 "sync"
8 "time"
9
10 "github.com/go-kit/kit/log"
11 "github.com/go-kit/kit/metrics"
12 "github.com/go-kit/kit/util/conn"
13 )
14
15 // Emitter is a struct to manage connections and orchestrate the emission of
16 // metrics to a Graphite system.
17 type Emitter struct {
18 mtx sync.Mutex
19 prefix string
20 mgr *conn.Manager
21 counters []*counter
22 histograms []*windowedHistogram
23 gauges []*gauge
24 logger log.Logger
25 quitc chan chan struct{}
26 }
27
28 // NewEmitter will return an Emitter that will prefix all metrics names with the
29 // given prefix. Once started, it will attempt to create a connection with the
30 // given network and address via `net.Dial` and periodically post metrics to the
31 // connection in the Graphite plaintext protocol.
32 func NewEmitter(network, address string, metricsPrefix string, flushInterval time.Duration, logger log.Logger) *Emitter {
33 return NewEmitterDial(net.Dial, network, address, metricsPrefix, flushInterval, logger)
34 }
35
36 // NewEmitterDial is the same as NewEmitter, but allows you to specify your own
37 // Dialer function. This is primarily useful for tests.
38 func NewEmitterDial(dialer conn.Dialer, network, address string, metricsPrefix string, flushInterval time.Duration, logger log.Logger) *Emitter {
39 e := &Emitter{
40 prefix: metricsPrefix,
41 mgr: conn.NewManager(dialer, network, address, time.After, logger),
42 logger: logger,
43 quitc: make(chan chan struct{}),
44 }
45 go e.loop(flushInterval)
46 return e
47 }
48
49 // NewCounter returns a Counter whose value will be periodically emitted in
50 // a Graphite-compatible format once the Emitter is started. Fields are ignored.
51 func (e *Emitter) NewCounter(name string) metrics.Counter {
52 e.mtx.Lock()
53 defer e.mtx.Unlock()
54 c := newCounter(name)
55 e.counters = append(e.counters, c)
56 return c
57 }
58
59 // NewHistogram is taken from http://github.com/codahale/metrics. It returns a
60 // windowed HDR histogram which drops data older than five minutes.
61 //
62 // The histogram exposes metrics for each passed quantile as gauges. Quantiles
63 // should be integers in the range 1..99. The gauge names are assigned by using
64 // the passed name as a prefix and appending "_pNN" e.g. "_p50".
65 //
66 // The values of this histogram will be periodically emitted in a
67 // Graphite-compatible format once the Emitter is started. Fields are ignored.
68 func (e *Emitter) NewHistogram(name string, minValue, maxValue int64, sigfigs int, quantiles ...int) (metrics.Histogram, error) {
69 gauges := map[int]metrics.Gauge{}
70 for _, quantile := range quantiles {
71 if quantile <= 0 || quantile >= 100 {
72 return nil, fmt.Errorf("invalid quantile %d", quantile)
73 }
74 gauges[quantile] = e.gauge(fmt.Sprintf("%s_p%02d", name, quantile))
75 }
76 h := newWindowedHistogram(name, minValue, maxValue, sigfigs, gauges, e.logger)
77
78 e.mtx.Lock()
79 defer e.mtx.Unlock()
80 e.histograms = append(e.histograms, h)
81 return h, nil
82 }
83
84 // NewGauge returns a Gauge whose value will be periodically emitted in a
85 // Graphite-compatible format once the Emitter is started. Fields are ignored.
86 func (e *Emitter) NewGauge(name string) metrics.Gauge {
87 e.mtx.Lock()
88 defer e.mtx.Unlock()
89 return e.gauge(name)
90 }
91
92 func (e *Emitter) gauge(name string) metrics.Gauge {
93 g := &gauge{name, 0}
94 e.gauges = append(e.gauges, g)
95 return g
96 }
97
98 func (e *Emitter) loop(d time.Duration) {
99 ticker := time.NewTicker(d)
100 defer ticker.Stop()
101
102 for {
103 select {
104 case <-ticker.C:
105 e.Flush()
106
107 case q := <-e.quitc:
108 e.Flush()
109 close(q)
110 return
111 }
112 }
113 }
114
115 // Stop will flush the current metrics and close the active connection. Calling
116 // stop more than once is a programmer error.
117 func (e *Emitter) Stop() {
118 q := make(chan struct{})
119 e.quitc <- q
120 <-q
121 }
122
123 // Flush will write the current metrics to the Emitter's connection in the
124 // Graphite plaintext protocol.
125 func (e *Emitter) Flush() {
126 e.mtx.Lock() // one flush at a time
127 defer e.mtx.Unlock()
128
129 conn := e.mgr.Take()
130 if conn == nil {
131 e.logger.Log("during", "flush", "err", "connection unavailable")
132 return
133 }
134
135 err := e.flush(conn)
136 if err != nil {
137 e.logger.Log("during", "flush", "err", err)
138 }
139 e.mgr.Put(err)
140 }
141
142 func (e *Emitter) flush(w io.Writer) error {
143 bw := bufio.NewWriter(w)
144
145 for _, c := range e.counters {
146 c.flush(bw, e.prefix)
147 }
148
149 for _, h := range e.histograms {
150 h.flush(bw, e.prefix)
151 }
152
153 for _, g := range e.gauges {
154 g.flush(bw, e.prefix)
155 }
156
157 return bw.Flush()
158 }
0 // Package graphite implements a Graphite backend for package metrics. Metrics
1 // will be emitted to a Graphite server in the plaintext protocol which looks
2 // like:
0 // Package graphite provides a Graphite backend for metrics. Metrics are batched
1 // and emitted in the plaintext protocol. For more information, see
2 // http://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol
33 //
4 // "<metric path> <metric value> <metric timestamp>"
5 //
6 // See http://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol.
7 // The current implementation ignores fields.
4 // Graphite does not have a native understanding of metric parameterization, so
5 // label values not supported. Use distinct metrics for each unique combination
6 // of label values.
87 package graphite
98
109 import (
1110 "fmt"
1211 "io"
13 "math"
14 "sort"
1512 "sync"
16 "sync/atomic"
1713 "time"
1814
19 "github.com/codahale/hdrhistogram"
20
2115 "github.com/go-kit/kit/log"
22 "github.com/go-kit/kit/metrics"
16 "github.com/go-kit/kit/metrics3"
17 "github.com/go-kit/kit/metrics3/generic"
18 "github.com/go-kit/kit/util/conn"
2319 )
2420
25 func newCounter(name string) *counter {
26 return &counter{name, 0}
27 }
28
29 func newGauge(name string) *gauge {
30 return &gauge{name, 0}
31 }
32
33 // counter implements the metrics.counter interface but also provides a
34 // Flush method to emit the current counter values in the Graphite plaintext
35 // protocol.
36 type counter struct {
37 key string
38 count uint64
39 }
40
41 func (c *counter) Name() string { return c.key }
42
43 // With currently ignores fields.
44 func (c *counter) With(metrics.Field) metrics.Counter { return c }
45
46 func (c *counter) Add(delta uint64) { atomic.AddUint64(&c.count, delta) }
47
48 func (c *counter) get() uint64 { return atomic.LoadUint64(&c.count) }
49
50 // flush will emit the current counter value in the Graphite plaintext
51 // protocol to the given io.Writer.
52 func (c *counter) flush(w io.Writer, prefix string) {
53 fmt.Fprintf(w, "%s.count %d %d\n", prefix+c.Name(), c.get(), time.Now().Unix())
54 }
55
56 // gauge implements the metrics.gauge interface but also provides a
57 // Flush method to emit the current counter values in the Graphite plaintext
58 // protocol.
59 type gauge struct {
60 key string
61 value uint64 // math.Float64bits
62 }
63
64 func (g *gauge) Name() string { return g.key }
65
66 // With currently ignores fields.
67 func (g *gauge) With(metrics.Field) metrics.Gauge { return g }
68
69 func (g *gauge) Add(delta float64) {
70 for {
71 old := atomic.LoadUint64(&g.value)
72 new := math.Float64bits(math.Float64frombits(old) + delta)
73 if atomic.CompareAndSwapUint64(&g.value, old, new) {
74 return
75 }
76 }
77 }
78
79 func (g *gauge) Set(value float64) {
80 atomic.StoreUint64(&g.value, math.Float64bits(value))
81 }
82
83 func (g *gauge) Get() float64 {
84 return math.Float64frombits(atomic.LoadUint64(&g.value))
85 }
86
87 // Flush will emit the current gauge value in the Graphite plaintext
88 // protocol to the given io.Writer.
89 func (g *gauge) flush(w io.Writer, prefix string) {
90 fmt.Fprintf(w, "%s %.2f %d\n", prefix+g.Name(), g.Get(), time.Now().Unix())
91 }
92
93 // windowedHistogram is taken from http://github.com/codahale/metrics. It
94 // is a windowed HDR histogram which drops data older than five minutes.
21 // Graphite receives metrics observations and forwards them to a Graphite server.
22 // Create a Graphite object, use it to create metrics, and pass those metrics as
23 // dependencies to the components that will use them.
9524 //
96 // The histogram exposes metrics for each passed quantile as gauges. Quantiles
97 // should be integers in the range 1..99. The gauge names are assigned by using
98 // the passed name as a prefix and appending "_pNN" e.g. "_p50".
25 // All metrics are buffered until WriteTo is called. Counters and gauges are
26 // aggregated into a single observation per timeseries per write. Histograms are
27 // exploded into per-quantile gauges and reported once per write.
9928 //
100 // The values of this histogram will be periodically emitted in a
101 // Graphite-compatible format once the GraphiteProvider is started. Fields are ignored.
102 type windowedHistogram struct {
103 mtx sync.Mutex
104 hist *hdrhistogram.WindowedHistogram
105
106 name string
107 gauges map[int]metrics.Gauge
108 logger log.Logger
109 }
110
111 func newWindowedHistogram(name string, minValue, maxValue int64, sigfigs int, quantiles map[int]metrics.Gauge, logger log.Logger) *windowedHistogram {
112 h := &windowedHistogram{
113 hist: hdrhistogram.NewWindowed(5, minValue, maxValue, sigfigs),
114 name: name,
115 gauges: quantiles,
116 logger: logger,
117 }
118 go h.rotateLoop(1 * time.Minute)
29 // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
30 // To send to a Graphite server, use the SendLoop helper method.
31 type Graphite struct {
32 mtx sync.RWMutex
33 prefix string
34 counters map[string]*Counter
35 gauges map[string]*Gauge
36 histograms map[string]*Histogram
37 logger log.Logger
38 }
39
40 // New returns a Statsd object that may be used to create metrics. Prefix is
41 // applied to all created metrics. Callers must ensure that regular calls to
42 // WriteTo are performed, either manually or with one of the helper methods.
43 func New(prefix string, logger log.Logger) *Graphite {
44 return &Graphite{
45 prefix: prefix,
46 counters: map[string]*Counter{},
47 gauges: map[string]*Gauge{},
48 histograms: map[string]*Histogram{},
49 logger: logger,
50 }
51 }
52
53 // NewCounter returns a counter. Observations are aggregated and emitted once
54 // per write invocation.
55 func (g *Graphite) NewCounter(name string) *Counter {
56 c := NewCounter(g.prefix + name)
57 g.mtx.Lock()
58 g.counters[g.prefix+name] = c
59 g.mtx.Unlock()
60 return c
61 }
62
63 // NewGauge returns a gauge. Observations are aggregated and emitted once per
64 // write invocation.
65 func (g *Graphite) NewGauge(name string) *Gauge {
66 ga := NewGauge(g.prefix + name)
67 g.mtx.Lock()
68 g.gauges[g.prefix+name] = ga
69 g.mtx.Unlock()
70 return ga
71 }
72
73 // NewHistogram returns a histogram. Observations are aggregated and emitted as
74 // per-quantile gauges, once per write invocation. 50 is a good default value
75 // for buckets.
76 func (g *Graphite) NewHistogram(name string, buckets int) *Histogram {
77 h := NewHistogram(g.prefix+name, buckets)
78 g.mtx.Lock()
79 g.histograms[g.prefix+name] = h
80 g.mtx.Unlock()
11981 return h
12082 }
12183
122 func (h *windowedHistogram) Name() string { return h.name }
123
124 func (h *windowedHistogram) With(metrics.Field) metrics.Histogram { return h }
125
126 func (h *windowedHistogram) Observe(value int64) {
127 h.mtx.Lock()
128 err := h.hist.Current.RecordValue(value)
129 h.mtx.Unlock()
130
131 if err != nil {
132 h.logger.Log("err", err, "msg", "unable to record histogram value")
133 return
134 }
135
136 for q, gauge := range h.gauges {
137 gauge.Set(float64(h.hist.Current.ValueAtQuantile(float64(q))))
138 }
139 }
140
141 func (h *windowedHistogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
142 bars := h.hist.Merge().Distribution()
143 buckets := make([]metrics.Bucket, len(bars))
144 for i, bar := range bars {
145 buckets[i] = metrics.Bucket{
146 From: bar.From,
147 To: bar.To,
148 Count: bar.Count,
149 }
150 }
151 quantiles := make([]metrics.Quantile, 0, len(h.gauges))
152 for quantile, gauge := range h.gauges {
153 quantiles = append(quantiles, metrics.Quantile{
154 Quantile: quantile,
155 Value: int64(gauge.Get()),
156 })
157 }
158 sort.Sort(quantileSlice(quantiles))
159 return buckets, quantiles
160 }
161
162 func (h *windowedHistogram) flush(w io.Writer, prefix string) {
163 name := prefix + h.Name()
164 hist := h.hist.Merge()
84 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
85 // time the passed channel fires. This method blocks until the channel is
86 // closed, so clients probably want to run it in its own goroutine. For typical
87 // usage, create a time.Ticker and pass its C channel to this method.
88 func (g *Graphite) WriteLoop(c <-chan time.Time, w io.Writer) {
89 for range c {
90 if _, err := g.WriteTo(w); err != nil {
91 g.logger.Log("during", "WriteTo", "err", err)
92 }
93 }
94 }
95
96 // SendLoop is a helper method that wraps WriteLoop, passing a managed
97 // connection to the network and address. Like WriteLoop, this method blocks
98 // until the channel is closed, so clients probably want to start it in its own
99 // goroutine. For typical usage, create a time.Ticker and pass its C channel to
100 // this method.
101 func (g *Graphite) SendLoop(c <-chan time.Time, network, address string) {
102 g.WriteLoop(c, conn.NewDefaultManager(network, address, g.logger))
103 }
104
105 // WriteTo flushes the buffered content of the metrics to the writer, in
106 // Graphite plaintext format. WriteTo abides best-effort semantics, so
107 // observations are lost if there is a problem with the write. Clients should be
108 // sure to call WriteTo regularly, ideally through the WriteLoop or SendLoop
109 // helper methods.
110 func (g *Graphite) WriteTo(w io.Writer) (count int64, err error) {
111 g.mtx.RLock()
112 defer g.mtx.RUnlock()
165113 now := time.Now().Unix()
166 fmt.Fprintf(w, "%s.count %d %d\n", name, hist.TotalCount(), now)
167 fmt.Fprintf(w, "%s.min %d %d\n", name, hist.Min(), now)
168 fmt.Fprintf(w, "%s.max %d %d\n", name, hist.Max(), now)
169 fmt.Fprintf(w, "%s.mean %.2f %d\n", name, hist.Mean(), now)
170 fmt.Fprintf(w, "%s.std-dev %.2f %d\n", name, hist.StdDev(), now)
171 }
172
173 func (h *windowedHistogram) rotateLoop(d time.Duration) {
174 for range time.Tick(d) {
175 h.mtx.Lock()
176 h.hist.Rotate()
177 h.mtx.Unlock()
178 }
179 }
180
181 type quantileSlice []metrics.Quantile
182
183 func (a quantileSlice) Len() int { return len(a) }
184 func (a quantileSlice) Less(i, j int) bool { return a[i].Quantile < a[j].Quantile }
185 func (a quantileSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
114
115 for name, c := range g.counters {
116 n, err := fmt.Fprintf(w, "%s %f %d\n", name, c.c.ValueReset(), now)
117 if err != nil {
118 return count, err
119 }
120 count += int64(n)
121 }
122
123 for name, ga := range g.gauges {
124 n, err := fmt.Fprintf(w, "%s %f %d\n", name, ga.g.Value(), now)
125 if err != nil {
126 return count, err
127 }
128 count += int64(n)
129 }
130
131 for name, h := range g.histograms {
132 for _, p := range []struct {
133 s string
134 f float64
135 }{
136 {"50", 0.50},
137 {"90", 0.90},
138 {"95", 0.95},
139 {"99", 0.99},
140 } {
141 n, err := fmt.Fprintf(w, "%s.p%s %f %d\n", name, p.s, h.h.Quantile(p.f), now)
142 if err != nil {
143 return count, err
144 }
145 count += int64(n)
146 }
147 }
148
149 return count, err
150 }
151
152 // Counter is a Graphite counter metric.
153 type Counter struct {
154 c *generic.Counter
155 }
156
157 // NewCounter returns a new usable counter metric.
158 func NewCounter(name string) *Counter {
159 return &Counter{generic.NewCounter(name)}
160 }
161
162 // With is a no-op.
163 func (c *Counter) With(...string) metrics.Counter { return c }
164
165 // Add implements counter.
166 func (c *Counter) Add(delta float64) { c.c.Add(delta) }
167
168 // Gauge is a Graphite gauge metric.
169 type Gauge struct {
170 g *generic.Gauge
171 }
172
173 // NewGauge returns a new usable Gauge metric.
174 func NewGauge(name string) *Gauge {
175 return &Gauge{generic.NewGauge(name)}
176 }
177
178 // With is a no-op.
179 func (g *Gauge) With(...string) metrics.Gauge { return g }
180
181 // Set implements gauge.
182 func (g *Gauge) Set(value float64) { g.g.Set(value) }
183
184 // Histogram is a Graphite histogram metric. Observations are bucketed into
185 // per-quantile gauges.
186 type Histogram struct {
187 h *generic.Histogram
188 }
189
190 // NewHistogram returns a new usable Histogram metric.
191 func NewHistogram(name string, buckets int) *Histogram {
192 return &Histogram{generic.NewHistogram(name, buckets)}
193 }
194
195 // With is a no-op.
196 func (h *Histogram) With(...string) metrics.Histogram { return h }
197
198 // Observe implements histogram.
199 func (h *Histogram) Observe(value float64) { h.h.Observe(value) }
11
22 import (
33 "bytes"
4 "fmt"
5 "strings"
4 "regexp"
5 "strconv"
66 "testing"
7 "time"
87
98 "github.com/go-kit/kit/log"
10 "github.com/go-kit/kit/metrics"
11 "github.com/go-kit/kit/metrics/teststat"
9 "github.com/go-kit/kit/metrics3/teststat"
1210 )
1311
14 func TestHistogramQuantiles(t *testing.T) {
15 prefix := "prefix."
16 e := NewEmitter("", "", prefix, time.Second, log.NewNopLogger())
17 var (
18 name = "test_histogram_quantiles"
19 quantiles = []int{50, 90, 95, 99}
20 )
21 h, err := e.NewHistogram(name, 0, 100, 3, quantiles...)
22 if err != nil {
23 t.Fatalf("unable to create test histogram: %v", err)
24 }
25 h = h.With(metrics.Field{Key: "ignored", Value: "field"})
26 const seed, mean, stdev int64 = 424242, 50, 10
27 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
28
29 // flush the current metrics into a buffer to examine
30 var b bytes.Buffer
31 e.flush(&b)
32 teststat.AssertGraphiteNormalHistogram(t, prefix, name, mean, stdev, quantiles, b.String())
33 }
34
3512 func TestCounter(t *testing.T) {
36 var (
37 prefix = "prefix."
38 name = "m"
39 value = 123
40 e = NewEmitter("", "", prefix, time.Second, log.NewNopLogger())
41 b bytes.Buffer
42 )
43 e.NewCounter(name).With(metrics.Field{Key: "ignored", Value: "field"}).Add(uint64(value))
44 e.flush(&b)
45 want := fmt.Sprintf("%s%s.count %d", prefix, name, value)
46 payload := b.String()
47 if !strings.HasPrefix(payload, want) {
48 t.Errorf("counter %s want\n%s, have\n%s", name, want, payload)
13 prefix, name := "abc.", "def"
14 label, value := "label", "value" // ignored for Graphite
15 regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
16 g := New(prefix, log.NewNopLogger())
17 counter := g.NewCounter(name).With(label, value)
18 valuef := teststat.SumLines(g, regex)
19 if err := teststat.TestCounter(counter, valuef); err != nil {
20 t.Fatal(err)
4921 }
5022 }
5123
5224 func TestGauge(t *testing.T) {
53 var (
54 prefix = "prefix."
55 name = "xyz"
56 value = 54321
57 delta = 12345
58 e = NewEmitter("", "", prefix, time.Second, log.NewNopLogger())
59 b bytes.Buffer
60 g = e.NewGauge(name).With(metrics.Field{Key: "ignored", Value: "field"})
61 )
62
63 g.Set(float64(value))
64 g.Add(float64(delta))
65
66 e.flush(&b)
67 payload := b.String()
68
69 want := fmt.Sprintf("%s%s %d", prefix, name, value+delta)
70 if !strings.HasPrefix(payload, want) {
71 t.Errorf("gauge %s want\n%s, have\n%s", name, want, payload)
25 prefix, name := "ghi.", "jkl"
26 label, value := "xyz", "abc" // ignored for Graphite
27 regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
28 g := New(prefix, log.NewNopLogger())
29 gauge := g.NewGauge(name).With(label, value)
30 valuef := teststat.LastLine(g, regex)
31 if err := teststat.TestGauge(gauge, valuef); err != nil {
32 t.Fatal(err)
7233 }
7334 }
7435
75 func TestEmitterStops(t *testing.T) {
76 e := NewEmitter("foo", "bar", "baz", time.Second, log.NewNopLogger())
77 time.Sleep(100 * time.Millisecond)
78 e.Stop()
36 func TestHistogram(t *testing.T) {
37 // The histogram test is actually like 4 gauge tests.
38 prefix, name := "statsd.", "histogram_test"
39 label, value := "abc", "def" // ignored for Graphite
40 re50 := regexp.MustCompile(prefix + name + `.p50 ([0-9\.]+) [0-9]+`)
41 re90 := regexp.MustCompile(prefix + name + `.p90 ([0-9\.]+) [0-9]+`)
42 re95 := regexp.MustCompile(prefix + name + `.p95 ([0-9\.]+) [0-9]+`)
43 re99 := regexp.MustCompile(prefix + name + `.p99 ([0-9\.]+) [0-9]+`)
44 g := New(prefix, log.NewNopLogger())
45 histogram := g.NewHistogram(name, 50).With(label, value)
46 quantiles := func() (float64, float64, float64, float64) {
47 var buf bytes.Buffer
48 g.WriteTo(&buf)
49 match50 := re50.FindStringSubmatch(buf.String())
50 p50, _ := strconv.ParseFloat(match50[1], 64)
51 match90 := re90.FindStringSubmatch(buf.String())
52 p90, _ := strconv.ParseFloat(match90[1], 64)
53 match95 := re95.FindStringSubmatch(buf.String())
54 p95, _ := strconv.ParseFloat(match95[1], 64)
55 match99 := re99.FindStringSubmatch(buf.String())
56 p99, _ := strconv.ParseFloat(match99[1], 64)
57 return p50, p90, p95, p99
58 }
59 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
60 t.Fatal(err)
61 }
7962 }
0 // Package influx provides an InfluxDB implementation for metrics. The model is
1 // similar to other push-based instrumentation systems. Observations are
2 // aggregated locally and emitted to the Influx server on regular intervals.
3 package influx
4
5 import (
6 "time"
7
8 influxdb "github.com/influxdata/influxdb/client/v2"
9
10 "github.com/go-kit/kit/log"
11 "github.com/go-kit/kit/metrics3"
12 "github.com/go-kit/kit/metrics3/internal/lv"
13 )
14
15 // Influx is a store for metrics that will be emitted to an Influx database.
16 //
17 // Influx is a general purpose time-series database, and has no native concepts
18 // of counters, gauges, or histograms. Counters are modeled as a timeseries with
19 // one data point per flush, with a "count" field that reflects all adds since
20 // the last flush. Gauges are modeled as a timeseries with one data point per
21 // flush, with a "value" field that reflects the current state of the gauge.
22 // Histograms are modeled as a timeseries with one data point per observation,
23 // with a "value" field that reflects each observation; use e.g. the HISTOGRAM
24 // aggregate function to compute histograms.
25 //
26 // Influx tags are immutable, attached to the Influx object, and given to each
27 // metric at construction. Influx fields are mapped to Go kit label values, and
28 // may be mutated via With functions. Actual metric values are provided as
29 // fields with specific names depending on the metric.
30 //
31 // All observations are collected in memory locally, and flushed on demand.
32 type Influx struct {
33 counters *lv.Space
34 gauges *lv.Space
35 histograms *lv.Space
36 tags map[string]string
37 conf influxdb.BatchPointsConfig
38 logger log.Logger
39 }
40
41 // New returns an Influx, ready to create metrics and collect observations. Tags
42 // are applied to all metrics created from this object. The BatchPointsConfig is
43 // used during flushing.
44 func New(tags map[string]string, conf influxdb.BatchPointsConfig, logger log.Logger) *Influx {
45 return &Influx{
46 counters: lv.NewSpace(),
47 gauges: lv.NewSpace(),
48 histograms: lv.NewSpace(),
49 tags: tags,
50 conf: conf,
51 logger: logger,
52 }
53 }
54
55 // NewCounter returns an Influx counter.
56 func (in *Influx) NewCounter(name string) *Counter {
57 return &Counter{
58 name: name,
59 obs: in.counters.Observe,
60 }
61 }
62
63 // NewGauge returns an Influx gauge.
64 func (in *Influx) NewGauge(name string) *Gauge {
65 return &Gauge{
66 name: name,
67 obs: in.gauges.Observe,
68 }
69 }
70
71 // NewHistogram returns an Influx histogram.
72 func (in *Influx) NewHistogram(name string) *Histogram {
73 return &Histogram{
74 name: name,
75 obs: in.histograms.Observe,
76 }
77 }
78
79 // BatchPointsWriter captures a subset of the influxdb.Client methods necessary
80 // for emitting metrics observations.
81 type BatchPointsWriter interface {
82 Write(influxdb.BatchPoints) error
83 }
84
85 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
86 // time the passed channel fires. This method blocks until the channel is
87 // closed, so clients probably want to run it in its own goroutine. For typical
88 // usage, create a time.Ticker and pass its C channel to this method.
89 func (in *Influx) WriteLoop(c <-chan time.Time, w BatchPointsWriter) {
90 for range c {
91 if err := in.WriteTo(w); err != nil {
92 in.logger.Log("during", "WriteTo", "err", err)
93 }
94 }
95 }
96
97 // WriteTo flushes the buffered content of the metrics to the writer, in an
98 // Influx BatchPoints format. WriteTo abides best-effort semantics, so
99 // observations are lost if there is a problem with the write. Clients should be
100 // sure to call WriteTo regularly, ideally through the WriteLoop helper method.
101 func (in *Influx) WriteTo(w BatchPointsWriter) (err error) {
102 bp, err := influxdb.NewBatchPoints(in.conf)
103 if err != nil {
104 return err
105 }
106
107 now := time.Now()
108
109 in.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
110 fields := fieldsFrom(lvs)
111 fields["count"] = sum(values)
112 var p *influxdb.Point
113 p, err = influxdb.NewPoint(name, in.tags, fields, now)
114 if err != nil {
115 return false
116 }
117 bp.AddPoint(p)
118 return true
119 })
120 if err != nil {
121 return err
122 }
123
124 in.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
125 fields := fieldsFrom(lvs)
126 fields["value"] = last(values)
127 var p *influxdb.Point
128 p, err = influxdb.NewPoint(name, in.tags, fields, now)
129 if err != nil {
130 return false
131 }
132 bp.AddPoint(p)
133 return true
134 })
135 if err != nil {
136 return err
137 }
138
139 in.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
140 fields := fieldsFrom(lvs)
141 ps := make([]*influxdb.Point, len(values))
142 for i, v := range values {
143 fields["value"] = v // overwrite each time
144 ps[i], err = influxdb.NewPoint(name, in.tags, fields, now)
145 if err != nil {
146 return false
147 }
148 }
149 bp.AddPoints(ps)
150 return true
151 })
152 if err != nil {
153 return err
154 }
155
156 return w.Write(bp)
157 }
158
159 func fieldsFrom(labelValues []string) map[string]interface{} {
160 if len(labelValues)%2 != 0 {
161 panic("fieldsFrom received a labelValues with an odd number of strings")
162 }
163 fields := make(map[string]interface{}, len(labelValues)/2)
164 for i := 0; i < len(labelValues); i += 2 {
165 fields[labelValues[i]] = labelValues[i+1]
166 }
167 return fields
168 }
169
170 func sum(a []float64) float64 {
171 var v float64
172 for _, f := range a {
173 v += f
174 }
175 return v
176 }
177
178 func last(a []float64) float64 {
179 return a[len(a)-1]
180 }
181
182 type observeFunc func(name string, lvs lv.LabelValues, value float64)
183
184 // Counter is an Influx counter. Observations are forwarded to an Influx
185 // object, and aggregated (summed) per timeseries.
186 type Counter struct {
187 name string
188 lvs lv.LabelValues
189 obs observeFunc
190 }
191
192 // With implements metrics.Counter.
193 func (c *Counter) With(labelValues ...string) metrics.Counter {
194 return &Counter{
195 name: c.name,
196 lvs: c.lvs.With(labelValues...),
197 obs: c.obs,
198 }
199 }
200
201 // Add implements metrics.Counter.
202 func (c *Counter) Add(delta float64) {
203 c.obs(c.name, c.lvs, delta)
204 }
205
206 // Gauge is an Influx gauge. Observations are forwarded to a Dogstatsd
207 // object, and aggregated (the last observation selected) per timeseries.
208 type Gauge struct {
209 name string
210 lvs lv.LabelValues
211 obs observeFunc
212 }
213
214 // With implements metrics.Gauge.
215 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
216 return &Gauge{
217 name: g.name,
218 lvs: g.lvs.With(labelValues...),
219 obs: g.obs,
220 }
221 }
222
223 // Set implements metrics.Gauge.
224 func (g *Gauge) Set(value float64) {
225 g.obs(g.name, g.lvs, value)
226 }
227
228 // Histogram is an Influx histrogram. Observations are aggregated into a
229 // generic.Histogram and emitted as per-quantile gauges to the Influx server.
230 type Histogram struct {
231 name string
232 lvs lv.LabelValues
233 obs observeFunc
234 }
235
236 // With implements metrics.Histogram.
237 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
238 return &Histogram{
239 name: h.name,
240 lvs: h.lvs.With(labelValues...),
241 obs: h.obs,
242 }
243 }
244
245 // Observe implements metrics.Histogram.
246 func (h *Histogram) Observe(value float64) {
247 h.obs(h.name, h.lvs, value)
248 }
0 package influx
1
2 import (
3 "bytes"
4 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8 "testing"
9
10 "github.com/go-kit/kit/log"
11 "github.com/go-kit/kit/metrics3/generic"
12 "github.com/go-kit/kit/metrics3/teststat"
13 influxdb "github.com/influxdata/influxdb/client/v2"
14 )
15
16 func TestCounter(t *testing.T) {
17 in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
18 re := regexp.MustCompile(`influx_counter,a=b count=([0-9\.]+) [0-9]+`) // reverse-engineered :\
19 counter := in.NewCounter("influx_counter")
20 value := func() float64 {
21 client := &bufWriter{}
22 in.WriteTo(client)
23 match := re.FindStringSubmatch(client.buf.String())
24 f, _ := strconv.ParseFloat(match[1], 64)
25 return f
26 }
27 if err := teststat.TestCounter(counter, value); err != nil {
28 t.Fatal(err)
29 }
30 }
31
32 func TestGauge(t *testing.T) {
33 in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
34 re := regexp.MustCompile(`influx_gauge,foo=alpha value=([0-9\.]+) [0-9]+`)
35 gauge := in.NewGauge("influx_gauge")
36 value := func() float64 {
37 client := &bufWriter{}
38 in.WriteTo(client)
39 match := re.FindStringSubmatch(client.buf.String())
40 f, _ := strconv.ParseFloat(match[1], 64)
41 return f
42 }
43 if err := teststat.TestGauge(gauge, value); err != nil {
44 t.Fatal(err)
45 }
46 }
47
48 func TestHistogram(t *testing.T) {
49 in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
50 re := regexp.MustCompile(`influx_histogram,foo=alpha bar="beta",value=([0-9\.]+) [0-9]+`)
51 histogram := in.NewHistogram("influx_histogram").With("bar", "beta")
52 quantiles := func() (float64, float64, float64, float64) {
53 w := &bufWriter{}
54 in.WriteTo(w)
55 h := generic.NewHistogram("h", 50)
56 matches := re.FindAllStringSubmatch(w.buf.String(), -1)
57 for _, match := range matches {
58 f, _ := strconv.ParseFloat(match[1], 64)
59 h.Observe(f)
60 }
61 return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99)
62 }
63 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
64 t.Fatal(err)
65 }
66 }
67
68 func TestHistogramLabels(t *testing.T) {
69 in := New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
70 h := in.NewHistogram("foo")
71 h.Observe(123)
72 h.With("abc", "xyz").Observe(456)
73 w := &bufWriter{}
74 if err := in.WriteTo(w); err != nil {
75 t.Fatal(err)
76 }
77 if want, have := 2, len(strings.Split(strings.TrimSpace(w.buf.String()), "\n")); want != have {
78 t.Errorf("want %d, have %d", want, have)
79 }
80 }
81
82 type bufWriter struct {
83 buf bytes.Buffer
84 }
85
86 func (w *bufWriter) Write(bp influxdb.BatchPoints) error {
87 for _, p := range bp.Points() {
88 fmt.Fprintf(&w.buf, p.String()+"\n")
89 }
90 return nil
91 }
+0
-254
metrics/influxdb/influxdb.go less more
0 // Package influxdb implements a InfluxDB backend for package metrics.
1 package influxdb
2
3 import (
4 "fmt"
5 "sort"
6 "sync"
7 "time"
8
9 "github.com/codahale/hdrhistogram"
10 stdinflux "github.com/influxdata/influxdb/client/v2"
11
12 "github.com/go-kit/kit/metrics"
13 )
14
15 type counter struct {
16 key string
17 tags []metrics.Field
18 fields []metrics.Field
19 value uint64
20 bp stdinflux.BatchPoints
21 }
22
23 // NewCounter returns a Counter that writes values in the reportInterval
24 // to the given InfluxDB client, utilizing batching.
25 func NewCounter(client stdinflux.Client, bp stdinflux.BatchPoints, key string, tags []metrics.Field, reportInterval time.Duration) metrics.Counter {
26 return NewCounterTick(client, bp, key, tags, time.Tick(reportInterval))
27 }
28
29 // NewCounterTick is the same as NewCounter, but allows the user to pass a own
30 // channel to trigger the write process to the client.
31 func NewCounterTick(client stdinflux.Client, bp stdinflux.BatchPoints, key string, tags []metrics.Field, reportTicker <-chan time.Time) metrics.Counter {
32 c := &counter{
33 key: key,
34 tags: tags,
35 value: 0,
36 bp: bp,
37 }
38 go watch(client, bp, reportTicker)
39 return c
40 }
41
42 func (c *counter) Name() string {
43 return c.key
44 }
45
46 func (c *counter) With(field metrics.Field) metrics.Counter {
47 return &counter{
48 key: c.key,
49 tags: c.tags,
50 value: c.value,
51 bp: c.bp,
52 fields: append(c.fields, field),
53 }
54 }
55
56 func (c *counter) Add(delta uint64) {
57 c.value = c.value + delta
58
59 tags := map[string]string{}
60
61 for _, tag := range c.tags {
62 tags[tag.Key] = tag.Value
63 }
64
65 fields := map[string]interface{}{}
66
67 for _, field := range c.fields {
68 fields[field.Key] = field.Value
69 }
70 fields["value"] = c.value
71 pt, _ := stdinflux.NewPoint(c.key, tags, fields, time.Now())
72 c.bp.AddPoint(pt)
73 }
74
75 type gauge struct {
76 key string
77 tags []metrics.Field
78 fields []metrics.Field
79 value float64
80 bp stdinflux.BatchPoints
81 }
82
83 // NewGauge creates a new gauge instance, reporting points in the defined reportInterval.
84 func NewGauge(client stdinflux.Client, bp stdinflux.BatchPoints, key string, tags []metrics.Field, reportInterval time.Duration) metrics.Gauge {
85 return NewGaugeTick(client, bp, key, tags, time.Tick(reportInterval))
86 }
87
88 // NewGaugeTick is the same as NewGauge with a ticker channel instead of a interval.
89 func NewGaugeTick(client stdinflux.Client, bp stdinflux.BatchPoints, key string, tags []metrics.Field, reportTicker <-chan time.Time) metrics.Gauge {
90 g := &gauge{
91 key: key,
92 tags: tags,
93 value: 0,
94 bp: bp,
95 }
96 go watch(client, bp, reportTicker)
97 return g
98 }
99
100 func (g *gauge) Name() string {
101 return g.key
102 }
103
104 func (g *gauge) With(field metrics.Field) metrics.Gauge {
105 return &gauge{
106 key: g.key,
107 tags: g.tags,
108 value: g.value,
109 bp: g.bp,
110 fields: append(g.fields, field),
111 }
112 }
113
114 func (g *gauge) Add(delta float64) {
115 g.value = g.value + delta
116 g.createPoint()
117 }
118
119 func (g *gauge) Set(value float64) {
120 g.value = value
121 g.createPoint()
122 }
123
124 func (g *gauge) Get() float64 {
125 return g.value
126 }
127
128 func (g *gauge) createPoint() {
129 tags := map[string]string{}
130
131 for _, tag := range g.tags {
132 tags[tag.Key] = tag.Value
133 }
134
135 fields := map[string]interface{}{}
136
137 for _, field := range g.fields {
138 fields[field.Key] = field.Value
139 }
140 fields["value"] = g.value
141 pt, _ := stdinflux.NewPoint(g.key, tags, fields, time.Now())
142 g.bp.AddPoint(pt)
143 }
144
145 // The implementation from histogram is taken from metrics/expvar
146
147 type histogram struct {
148 mu sync.Mutex
149 hist *hdrhistogram.WindowedHistogram
150
151 key string
152 gauges map[int]metrics.Gauge
153 }
154
155 // NewHistogram is taken from http://github.com/codahale/metrics. It returns a
156 // windowed HDR histogram which drops data older than five minutes.
157 //
158 // The histogram exposes metrics for each passed quantile as gauges. Quantiles
159 // should be integers in the range 1..99. The gauge names are assigned by
160 // using the passed name as a prefix and appending "_pNN" e.g. "_p50".
161 func NewHistogram(client stdinflux.Client, bp stdinflux.BatchPoints, key string, tags []metrics.Field,
162 reportInterval time.Duration, minValue, maxValue int64, sigfigs int, quantiles ...int) metrics.Histogram {
163 return NewHistogramTick(client, bp, key, tags, time.Tick(reportInterval), minValue, maxValue, sigfigs, quantiles...)
164 }
165
166 // NewHistogramTick is the same as NewHistoGram, but allows to pass a custom reportTicker.
167 func NewHistogramTick(client stdinflux.Client, bp stdinflux.BatchPoints, key string, tags []metrics.Field,
168 reportTicker <-chan time.Time, minValue, maxValue int64, sigfigs int, quantiles ...int) metrics.Histogram {
169 gauges := map[int]metrics.Gauge{}
170
171 for _, quantile := range quantiles {
172 if quantile <= 0 || quantile >= 100 {
173 panic(fmt.Sprintf("invalid quantile %d", quantile))
174 }
175 gauges[quantile] = NewGaugeTick(client, bp, fmt.Sprintf("%s_p%02d", key, quantile), tags, reportTicker)
176 }
177
178 h := &histogram{
179 hist: hdrhistogram.NewWindowed(5, minValue, maxValue, sigfigs),
180 key: key,
181 gauges: gauges,
182 }
183
184 go h.rotateLoop(1 * time.Minute)
185 return h
186 }
187
188 func (h *histogram) Name() string {
189 return h.key
190 }
191
192 func (h *histogram) With(field metrics.Field) metrics.Histogram {
193 for q, gauge := range h.gauges {
194 h.gauges[q] = gauge.With(field)
195 }
196
197 return h
198 }
199
200 func (h *histogram) Observe(value int64) {
201 h.mu.Lock()
202 err := h.hist.Current.RecordValue(value)
203 h.mu.Unlock()
204
205 if err != nil {
206 panic(err.Error())
207 }
208
209 for q, gauge := range h.gauges {
210 gauge.Set(float64(h.hist.Current.ValueAtQuantile(float64(q))))
211 }
212 }
213
214 func (h *histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
215 bars := h.hist.Merge().Distribution()
216 buckets := make([]metrics.Bucket, len(bars))
217 for i, bar := range bars {
218 buckets[i] = metrics.Bucket{
219 From: bar.From,
220 To: bar.To,
221 Count: bar.Count,
222 }
223 }
224 quantiles := make([]metrics.Quantile, 0, len(h.gauges))
225 for quantile, gauge := range h.gauges {
226 quantiles = append(quantiles, metrics.Quantile{
227 Quantile: quantile,
228 Value: int64(gauge.Get()),
229 })
230 }
231 sort.Sort(quantileSlice(quantiles))
232 return buckets, quantiles
233 }
234
235 func (h *histogram) rotateLoop(d time.Duration) {
236 for range time.Tick(d) {
237 h.mu.Lock()
238 h.hist.Rotate()
239 h.mu.Unlock()
240 }
241 }
242
243 type quantileSlice []metrics.Quantile
244
245 func (a quantileSlice) Len() int { return len(a) }
246 func (a quantileSlice) Less(i, j int) bool { return a[i].Quantile < a[j].Quantile }
247 func (a quantileSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
248
249 func watch(client stdinflux.Client, bp stdinflux.BatchPoints, reportTicker <-chan time.Time) {
250 for range reportTicker {
251 client.Write(bp)
252 }
253 }
+0
-348
metrics/influxdb/influxdb_test.go less more
0 package influxdb_test
1
2 import (
3 "reflect"
4 "sync"
5 "testing"
6 "time"
7
8 stdinflux "github.com/influxdata/influxdb/client/v2"
9
10 "github.com/go-kit/kit/metrics"
11 "github.com/go-kit/kit/metrics/influxdb"
12 )
13
14 func TestCounter(t *testing.T) {
15 expectedName := "test_counter"
16 expectedTags := map[string]string{}
17 expectedFields := []map[string]interface{}{
18 {"value": "2"},
19 {"value": "7"},
20 {"value": "10"},
21 }
22
23 cl := &mockClient{}
24 cl.Add(3)
25 bp, _ := stdinflux.NewBatchPoints(stdinflux.BatchPointsConfig{
26 Database: "testing",
27 Precision: "s",
28 })
29
30 tags := []metrics.Field{}
31 for key, value := range expectedTags {
32 tags = append(tags, metrics.Field{Key: key, Value: value})
33 }
34
35 triggerChan := make(chan time.Time)
36 counter := influxdb.NewCounterTick(cl, bp, expectedName, tags, triggerChan)
37 counter.Add(2)
38 counter.Add(5)
39 counter.Add(3)
40
41 triggerChan <- time.Now()
42 cl.Wait()
43
44 for i := 0; i <= 2; i++ {
45 givenPoint := mockPoint{
46 Name: expectedName,
47 Tags: expectedTags,
48 Fields: expectedFields[i],
49 }
50 comparePoint(t, i, givenPoint, cl.Points[i])
51 }
52 }
53
54 func TestCounterWithTags(t *testing.T) {
55 expectedName := "test_counter"
56 expectedTags := map[string]string{
57 "key1": "value1",
58 "key2": "value2",
59 }
60 expectedFields := []map[string]interface{}{
61 {"value": "2"},
62 {"Test": "Test", "value": "7"},
63 {"Test": "Test", "value": "10"},
64 }
65
66 cl := &mockClient{}
67 cl.Add(3)
68 bp, _ := stdinflux.NewBatchPoints(stdinflux.BatchPointsConfig{
69 Database: "testing",
70 Precision: "s",
71 })
72
73 tags := []metrics.Field{}
74 for key, value := range expectedTags {
75 tags = append(tags, metrics.Field{Key: key, Value: value})
76 }
77
78 triggerChan := make(chan time.Time)
79 counter := influxdb.NewCounterTick(cl, bp, expectedName, tags, triggerChan)
80 counter.Add(2)
81 counter = counter.With(metrics.Field{Key: "Test", Value: "Test"})
82 counter.Add(5)
83 counter.Add(3)
84
85 triggerChan <- time.Now()
86 cl.Wait()
87
88 for i := 0; i <= 2; i++ {
89 givenPoint := mockPoint{
90 Name: expectedName,
91 Tags: expectedTags,
92 Fields: expectedFields[i],
93 }
94 comparePoint(t, i, givenPoint, cl.Points[i])
95 }
96 }
97
98 func TestGauge(t *testing.T) {
99 expectedName := "test_gauge"
100 expectedTags := map[string]string{}
101 expectedFields := []map[string]interface{}{
102 {"value": 2.1},
103 {"value": 1.0},
104 {"value": 10.5},
105 }
106
107 cl := &mockClient{}
108 cl.Add(3)
109 bp, _ := stdinflux.NewBatchPoints(stdinflux.BatchPointsConfig{
110 Database: "testing",
111 Precision: "s",
112 })
113
114 tags := []metrics.Field{}
115 for key, value := range expectedTags {
116 tags = append(tags, metrics.Field{Key: key, Value: value})
117 }
118
119 triggerChan := make(chan time.Time)
120 counter := influxdb.NewGaugeTick(cl, bp, expectedName, tags, triggerChan)
121 counter.Add(2.1)
122 counter.Set(1)
123 counter.Add(9.5)
124
125 triggerChan <- time.Now()
126 cl.Wait()
127
128 for i := 0; i <= 2; i++ {
129 givenPoint := mockPoint{
130 Name: expectedName,
131 Tags: expectedTags,
132 Fields: expectedFields[i],
133 }
134 comparePoint(t, i, givenPoint, cl.Points[i])
135 }
136 }
137
138 func TestGaugeWithTags(t *testing.T) {
139 expectedName := "test_counter"
140 expectedTags := map[string]string{
141 "key1": "value1",
142 "key2": "value2",
143 }
144 expectedFields := []map[string]interface{}{
145 {"value": 2.3},
146 {"Test": "Test", "value": 1.0},
147 {"Test": "Test", "value": 13.6},
148 }
149
150 cl := &mockClient{}
151 cl.Add(3)
152 bp, _ := stdinflux.NewBatchPoints(stdinflux.BatchPointsConfig{
153 Database: "testing",
154 Precision: "s",
155 })
156
157 tags := []metrics.Field{}
158 for key, value := range expectedTags {
159 tags = append(tags, metrics.Field{Key: key, Value: value})
160 }
161
162 triggerChan := make(chan time.Time)
163 gauge := influxdb.NewGaugeTick(cl, bp, expectedName, tags, triggerChan)
164 gauge.Add(2.3)
165 gauge = gauge.With(metrics.Field{Key: "Test", Value: "Test"})
166 gauge.Set(1)
167 gauge.Add(12.6)
168
169 triggerChan <- time.Now()
170 cl.Wait()
171
172 for i := 0; i <= 2; i++ {
173 givenPoint := mockPoint{
174 Name: expectedName,
175 Tags: expectedTags,
176 Fields: expectedFields[i],
177 }
178 comparePoint(t, i, givenPoint, cl.Points[i])
179 }
180 }
181
182 func TestHistogram(t *testing.T) {
183 expectedName := "test_histogram"
184 expectedTags := map[string]string{}
185 expectedFields := []map[string]map[string]interface{}{
186 {
187 "test_histogram_p50": {"value": 5.0},
188 "test_histogram_p90": {"value": 5.0},
189 "test_histogram_p95": {"value": 5.0},
190 "test_histogram_p99": {"value": 5.0},
191 },
192 {
193 "test_histogram_p50": {"Test": "Test", "value": 5.0},
194 "test_histogram_p90": {"Test": "Test", "value": 10.0},
195 "test_histogram_p95": {"Test": "Test", "value": 10.0},
196 "test_histogram_p99": {"Test": "Test", "value": 10.0},
197 },
198 {
199 "test_histogram_p50": {"Test": "Test", "value": 5.0},
200 "test_histogram_p90": {"Test": "Test", "value": 10.0},
201 "test_histogram_p95": {"Test": "Test", "value": 10.0},
202 "test_histogram_p99": {"Test": "Test", "value": 10.0},
203 },
204 }
205 quantiles := []int{50, 90, 95, 99}
206
207 cl := &mockClient{}
208 cl.Add(12)
209 bp, _ := stdinflux.NewBatchPoints(stdinflux.BatchPointsConfig{
210 Database: "testing",
211 Precision: "s",
212 })
213
214 tags := []metrics.Field{}
215 for key, value := range expectedTags {
216 tags = append(tags, metrics.Field{Key: key, Value: value})
217 }
218
219 triggerChan := make(chan time.Time)
220 histogram := influxdb.NewHistogramTick(cl, bp, expectedName, tags, triggerChan, 0, 100, 3, quantiles...)
221 histogram.Observe(5)
222 histogram = histogram.With(metrics.Field{Key: "Test", Value: "Test"})
223 histogram.Observe(10)
224 histogram.Observe(4)
225 triggerChan <- time.Now()
226 cl.Wait()
227
228 for i := 0; i <= 11; i++ {
229 actualName := cl.Points[i].Name()
230 givenName := expectedName + actualName[len(actualName)-4:]
231 givenPoint := mockPoint{
232 Name: givenName,
233 Tags: expectedTags,
234 Fields: expectedFields[i/4][actualName],
235 }
236 comparePoint(t, i, givenPoint, cl.Points[i])
237 }
238 }
239
240 func TestHistogramWithTags(t *testing.T) {
241 expectedName := "test_histogram"
242 expectedTags := map[string]string{
243 "key1": "value1",
244 "key2": "value2",
245 }
246 expectedFields := []map[string]map[string]interface{}{
247 {
248 "test_histogram_p50": {"value": 5.0},
249 "test_histogram_p90": {"value": 5.0},
250 "test_histogram_p95": {"value": 5.0},
251 "test_histogram_p99": {"value": 5.0},
252 },
253 {
254 "test_histogram_p50": {"Test": "Test", "value": 5.0},
255 "test_histogram_p90": {"Test": "Test", "value": 10.0},
256 "test_histogram_p95": {"Test": "Test", "value": 10.0},
257 "test_histogram_p99": {"Test": "Test", "value": 10.0},
258 },
259 {
260 "test_histogram_p50": {"Test": "Test", "value": 5.0},
261 "test_histogram_p90": {"Test": "Test", "value": 10.0},
262 "test_histogram_p95": {"Test": "Test", "value": 10.0},
263 "test_histogram_p99": {"Test": "Test", "value": 10.0},
264 },
265 }
266 quantiles := []int{50, 90, 95, 99}
267
268 cl := &mockClient{}
269 cl.Add(12)
270 bp, _ := stdinflux.NewBatchPoints(stdinflux.BatchPointsConfig{
271 Database: "testing",
272 Precision: "s",
273 })
274
275 tags := []metrics.Field{}
276 for key, value := range expectedTags {
277 tags = append(tags, metrics.Field{Key: key, Value: value})
278 }
279
280 triggerChan := make(chan time.Time)
281 histogram := influxdb.NewHistogramTick(cl, bp, expectedName, tags, triggerChan, 0, 100, 3, quantiles...)
282 histogram.Observe(5)
283 histogram = histogram.With(metrics.Field{Key: "Test", Value: "Test"})
284 histogram.Observe(10)
285 histogram.Observe(4)
286 triggerChan <- time.Now()
287 cl.Wait()
288
289 for i := 0; i <= 11; i++ {
290 actualName := cl.Points[i].Name()
291 givenName := expectedName + actualName[len(actualName)-4:]
292 givenPoint := mockPoint{
293 Name: givenName,
294 Tags: expectedTags,
295 Fields: expectedFields[i/4][actualName],
296 }
297 comparePoint(t, i, givenPoint, cl.Points[i])
298 }
299 }
300
301 func comparePoint(t *testing.T, i int, expected mockPoint, given stdinflux.Point) {
302
303 if want, have := expected.Name, given.Name(); want != have {
304 t.Errorf("point %d: want %q, have %q", i, want, have)
305 }
306
307 if want, have := expected.Tags, given.Tags(); !reflect.DeepEqual(want, have) {
308 t.Errorf("point %d: want %v, have %v", i, want, have)
309 }
310
311 if want, have := expected.Fields, given.Fields(); !reflect.DeepEqual(want, have) {
312 t.Errorf("point %d: want %v, have %v", i, want, have)
313 }
314 }
315
316 type mockClient struct {
317 Points []stdinflux.Point
318 sync.WaitGroup
319 }
320
321 func (m *mockClient) Ping(timeout time.Duration) (time.Duration, string, error) {
322 t := 0 * time.Millisecond
323 return t, "", nil
324 }
325
326 func (m *mockClient) Write(bp stdinflux.BatchPoints) error {
327 for _, p := range bp.Points() {
328 m.Points = append(m.Points, *p)
329 m.Done()
330 }
331
332 return nil
333 }
334
335 func (m *mockClient) Query(q stdinflux.Query) (*stdinflux.Response, error) {
336 return nil, nil
337 }
338
339 func (m *mockClient) Close() error {
340 return nil
341 }
342
343 type mockPoint struct {
344 Name string
345 Tags map[string]string
346 Fields map[string]interface{}
347 }
0 package emitting
1
2 import (
3 "fmt"
4 "strings"
5 "sync"
6
7 "sort"
8
9 "github.com/go-kit/kit/metrics3/generic"
10 )
11
12 type Buffer struct {
13 buckets int
14
15 mtx sync.Mutex
16 counters map[point]*generic.Counter
17 gauges map[point]*generic.Gauge
18 histograms map[point]*generic.Histogram
19 }
20
21 func (b *Buffer) Add(a Add) {
22 pt := makePoint(a.Name, a.LabelValues)
23 b.mtx.Lock()
24 defer b.mtx.Unlock()
25 c, ok := b.counters[pt]
26 if !ok {
27 c = generic.NewCounter(a.Name).With(a.LabelValues...).(*generic.Counter)
28 }
29 c.Add(a.Delta)
30 b.counters[pt] = c
31 }
32
33 func (b *Buffer) Set(s Set) {
34 pt := makePoint(s.Name, s.LabelValues)
35 b.mtx.Lock()
36 defer b.mtx.Unlock()
37 g, ok := b.gauges[pt]
38 if !ok {
39 g = generic.NewGauge(s.Name).With(s.LabelValues...).(*generic.Gauge)
40 }
41 g.Set(s.Value)
42 b.gauges[pt] = g
43 }
44
45 func (b *Buffer) Obv(o Obv) {
46 pt := makePoint(o.Name, o.LabelValues)
47 b.mtx.Lock()
48 defer b.mtx.Unlock()
49 h, ok := b.histograms[pt]
50 if !ok {
51 h = generic.NewHistogram(o.Name, b.buckets).With(o.LabelValues...).(*generic.Histogram)
52 }
53 h.Observe(o.Value)
54 b.histograms[pt] = h
55 }
56
57 // point as in point in N-dimensional vector space;
58 // a string encoding of name + sorted k/v pairs.
59 type point string
60
61 const (
62 recordDelimiter = "•"
63 fieldDelimiter = "·"
64 )
65
66 // (foo, [a b c d]) => "foo•a·b•c·d"
67 func makePoint(name string, labelValues []string) point {
68 if len(labelValues)%2 != 0 {
69 panic("odd number of label values; programmer error!")
70 }
71 pairs := make([]string, 0, len(labelValues)/2)
72 for i := 0; i < len(labelValues); i += 2 {
73 pairs = append(pairs, fmt.Sprintf("%s%s%s", labelValues[i], fieldDelimiter, labelValues[i+1]))
74 }
75 sort.Strings(sort.StringSlice(pairs))
76 pairs = append([]string{name}, pairs...)
77 return point(strings.Join(pairs, recordDelimiter))
78 }
79
80 // "foo•a·b•c·d" => (foo, [a b c d])
81 func (p point) nameLabelValues() (name string, labelValues []string) {
82 records := strings.Split(string(p), recordDelimiter)
83 if len(records)%2 != 1 { // always name + even number of label/values
84 panic("even number of point records; programmer error!")
85 }
86 name, records = records[0], records[1:]
87 labelValues = make([]string, 0, len(records)*2)
88 for _, record := range records {
89 fields := strings.SplitN(record, fieldDelimiter, 2)
90 labelValues = append(labelValues, fields[0], fields[1])
91 }
92 return name, labelValues
93 }
0 package emitting
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/internal/lv"
5 )
6
7 type Counter struct {
8 name string
9 lvs lv.LabelValues
10 sampleRate float64
11 c chan Add
12 }
13
14 type Add struct {
15 Name string
16 LabelValues []string
17 SampleRate float64
18 Delta float64
19 }
20
21 func NewCounter(name string, sampleRate float64, c chan Add) *Counter {
22 return &Counter{
23 name: name,
24 sampleRate: sampleRate,
25 c: c,
26 }
27 }
28
29 func (c *Counter) With(labelValues ...string) metrics.Counter {
30 return &Counter{
31 name: c.name,
32 lvs: c.lvs.With(labelValues...),
33 sampleRate: c.sampleRate,
34 c: c.c,
35 }
36 }
37
38 func (c *Counter) Add(delta float64) {
39 c.c <- Add{c.name, c.lvs, c.sampleRate, delta}
40 }
41
42 type Gauge struct {
43 name string
44 lvs lv.LabelValues
45 c chan Set
46 }
47
48 type Set struct {
49 Name string
50 LabelValues []string
51 Value float64
52 }
53
54 func NewGauge(name string, c chan Set) *Gauge {
55 return &Gauge{
56 name: name,
57 c: c,
58 }
59 }
60
61 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
62 return &Gauge{
63 name: g.name,
64 lvs: g.lvs.With(labelValues...),
65 c: g.c,
66 }
67 }
68
69 func (g *Gauge) Set(value float64) {
70 g.c <- Set{g.name, g.lvs, value}
71 }
72
73 type Histogram struct {
74 name string
75 lvs lv.LabelValues
76 sampleRate float64
77 c chan Obv
78 }
79
80 type Obv struct {
81 Name string
82 LabelValues []string
83 SampleRate float64
84 Value float64
85 }
86
87 func NewHistogram(name string, sampleRate float64, c chan Obv) *Histogram {
88 return &Histogram{
89 name: name,
90 sampleRate: sampleRate,
91 c: c,
92 }
93 }
94
95 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
96 return &Histogram{
97 name: h.name,
98 lvs: h.lvs.With(labelValues...),
99 sampleRate: h.sampleRate,
100 c: h.c,
101 }
102 }
103
104 func (h *Histogram) Observe(value float64) {
105 h.c <- Obv{h.name, h.lvs, h.sampleRate, value}
106 }
0 package lv
1
2 // LabelValues is a type alias that provides validation on its With method.
3 // Metrics may include it as a member to help them satisfy With semantics and
4 // save some code duplication.
5 type LabelValues []string
6
7 // With validates the input, and returns a new aggregate labelValues.
8 func (lvs LabelValues) With(labelValues ...string) LabelValues {
9 if len(labelValues)%2 != 0 {
10 labelValues = append(labelValues, "unknown")
11 }
12 return append(lvs, labelValues...)
13 }
0 package lv
1
2 import (
3 "strings"
4 "testing"
5 )
6
7 func TestWith(t *testing.T) {
8 var a LabelValues
9 b := a.With("a", "1")
10 c := a.With("b", "2", "c", "3")
11
12 if want, have := "", strings.Join(a, ""); want != have {
13 t.Errorf("With appears to mutate the original LabelValues: want %q, have %q", want, have)
14 }
15 if want, have := "a1", strings.Join(b, ""); want != have {
16 t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
17 }
18 if want, have := "b2c3", strings.Join(c, ""); want != have {
19 t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
20 }
21 }
0 package lv
1
2 import "sync"
3
4 // NewSpace returns an N-dimensional vector space.
5 func NewSpace() *Space {
6 return &Space{}
7 }
8
9 // Space represents an N-dimensional vector space. Each name and unique label
10 // value pair establishes a new dimension and point within that dimension. Order
11 // matters, i.e. [a=1 b=2] identifies a different timeseries than [b=2 a=1].
12 type Space struct {
13 mtx sync.RWMutex
14 nodes map[string]*node
15 }
16
17 // Observe locates the time series identified by the name and label values in
18 // the vector space, and appends the value to the list of observations.
19 func (s *Space) Observe(name string, lvs LabelValues, value float64) {
20 s.nodeFor(name).observe(lvs, value)
21 }
22
23 // Walk traverses the vector space and invokes fn for each non-empty time series
24 // which is encountered. Return false to abort the traversal.
25 func (s *Space) Walk(fn func(name string, lvs LabelValues, observations []float64) bool) {
26 s.mtx.RLock()
27 defer s.mtx.RUnlock()
28 for name, node := range s.nodes {
29 f := func(lvs LabelValues, observations []float64) bool { return fn(name, lvs, observations) }
30 if !node.walk(LabelValues{}, f) {
31 return
32 }
33 }
34 }
35
36 // Reset empties the current space and returns a new Space with the old
37 // contents. Reset a Space to get an immutable copy suitable for walking.
38 func (s *Space) Reset() *Space {
39 s.mtx.Lock()
40 defer s.mtx.Unlock()
41 n := NewSpace()
42 n.nodes, s.nodes = s.nodes, n.nodes
43 return n
44 }
45
46 func (s *Space) nodeFor(name string) *node {
47 s.mtx.Lock()
48 defer s.mtx.Unlock()
49 if s.nodes == nil {
50 s.nodes = map[string]*node{}
51 }
52 n, ok := s.nodes[name]
53 if !ok {
54 n = &node{}
55 s.nodes[name] = n
56 }
57 return n
58 }
59
60 // node exists at a specific point in the N-dimensional vector space of all
61 // possible label values. The node collects observations and has child nodes
62 // with greater specificity.
63 type node struct {
64 mtx sync.RWMutex
65 observations []float64
66 children map[pair]*node
67 }
68
69 type pair struct{ label, value string }
70
71 func (n *node) observe(lvs LabelValues, value float64) {
72 n.mtx.Lock()
73 defer n.mtx.Unlock()
74 if len(lvs) == 0 {
75 n.observations = append(n.observations, value)
76 return
77 }
78 if len(lvs) < 2 {
79 panic("too few LabelValues; programmer error!")
80 }
81 head, tail := pair{lvs[0], lvs[1]}, lvs[2:]
82 if n.children == nil {
83 n.children = map[pair]*node{}
84 }
85 child, ok := n.children[head]
86 if !ok {
87 child = &node{}
88 n.children[head] = child
89 }
90 child.observe(tail, value)
91 }
92
93 func (n *node) walk(lvs LabelValues, fn func(LabelValues, []float64) bool) bool {
94 n.mtx.RLock()
95 defer n.mtx.RUnlock()
96 if len(n.observations) > 0 && !fn(lvs, n.observations) {
97 return false
98 }
99 for p, child := range n.children {
100 if !child.walk(append(lvs, p.label, p.value), fn) {
101 return false
102 }
103 }
104 return true
105 }
0 package lv
1
2 import (
3 "strings"
4 "testing"
5 )
6
7 func TestSpaceWalkAbort(t *testing.T) {
8 s := NewSpace()
9 s.Observe("a", LabelValues{"a", "b"}, 1)
10 s.Observe("a", LabelValues{"c", "d"}, 2)
11 s.Observe("a", LabelValues{"e", "f"}, 4)
12 s.Observe("a", LabelValues{"g", "h"}, 8)
13 s.Observe("b", LabelValues{"a", "b"}, 16)
14 s.Observe("b", LabelValues{"c", "d"}, 32)
15 s.Observe("b", LabelValues{"e", "f"}, 64)
16 s.Observe("b", LabelValues{"g", "h"}, 128)
17
18 var count int
19 s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
20 count++
21 return false
22 })
23 if want, have := 1, count; want != have {
24 t.Errorf("want %d, have %d", want, have)
25 }
26 }
27
28 func TestSpaceWalkSums(t *testing.T) {
29 s := NewSpace()
30 s.Observe("metric_one", LabelValues{}, 1)
31 s.Observe("metric_one", LabelValues{}, 2)
32 s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 4)
33 s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 8)
34 s.Observe("metric_one", LabelValues{}, 16)
35 s.Observe("metric_one", LabelValues{"a", "1", "b", "3"}, 32)
36 s.Observe("metric_two", LabelValues{}, 64)
37 s.Observe("metric_two", LabelValues{}, 128)
38 s.Observe("metric_two", LabelValues{"a", "1", "b", "2"}, 256)
39
40 have := map[string]float64{}
41 s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
42 //t.Logf("%s %v => %v", name, lvs, obs)
43 have[name+" ["+strings.Join(lvs, "")+"]"] += sum(obs)
44 return true
45 })
46
47 want := map[string]float64{
48 "metric_one []": 1 + 2 + 16,
49 "metric_one [a1b2]": 4 + 8,
50 "metric_one [a1b3]": 32,
51 "metric_two []": 64 + 128,
52 "metric_two [a1b2]": 256,
53 }
54 for keystr, wantsum := range want {
55 if havesum := have[keystr]; wantsum != havesum {
56 t.Errorf("%q: want %.1f, have %.1f", keystr, wantsum, havesum)
57 }
58 delete(want, keystr)
59 delete(have, keystr)
60 }
61 for keystr, havesum := range have {
62 t.Errorf("%q: unexpected observations recorded: %.1f", keystr, havesum)
63 }
64 }
65
66 func TestSpaceWalkSkipsEmptyDimensions(t *testing.T) {
67 s := NewSpace()
68 s.Observe("foo", LabelValues{"bar", "1", "baz", "2"}, 123)
69
70 var count int
71 s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
72 count++
73 return true
74 })
75 if want, have := 1, count; want != have {
76 t.Errorf("want %d, have %d", want, have)
77 }
78 }
79
80 func sum(a []float64) (v float64) {
81 for _, f := range a {
82 v += f
83 }
84 return
85 }
0 // Package ratemap implements a goroutine-safe map of string to float64. It can
1 // be embedded in implementations whose metrics support fixed sample rates, so
2 // that an additional parameter doesn't have to be tracked through the e.g.
3 // lv.Space object.
4 package ratemap
5
6 import "sync"
7
8 // RateMap is a simple goroutine-safe map of string to float64.
9 type RateMap struct {
10 mtx sync.RWMutex
11 m map[string]float64
12 }
13
14 // New returns a new RateMap.
15 func New() *RateMap {
16 return &RateMap{
17 m: map[string]float64{},
18 }
19 }
20
21 // Set writes the given name/rate pair to the map.
22 // Set is safe for concurrent access by multiple goroutines.
23 func (m *RateMap) Set(name string, rate float64) {
24 m.mtx.Lock()
25 defer m.mtx.Unlock()
26 m.m[name] = rate
27 }
28
29 // Get retrieves the rate for the given name, or 1.0 if none is set.
30 // Get is safe for concurrent access by multiple goroutines.
31 func (m *RateMap) Get(name string) float64 {
32 m.mtx.RLock()
33 defer m.mtx.RUnlock()
34 f, ok := m.m[name]
35 if !ok {
36 f = 1.0
37 }
38 return f
39 }
00 package metrics
11
2 // Counter is a monotonically-increasing, unsigned, 64-bit integer used to
3 // capture the number of times an event has occurred. By tracking the deltas
4 // between measurements of a counter over intervals of time, an aggregation
5 // layer can derive rates, acceleration, etc.
2 // Counter describes a metric that accumulates values monotonically.
3 // An example of a counter is the number of received HTTP requests.
64 type Counter interface {
7 Name() string
8 With(Field) Counter
9 Add(delta uint64)
5 With(labelValues ...string) Counter
6 Add(delta float64)
107 }
118
12 // Gauge captures instantaneous measurements of something using signed, 64-bit
13 // floats. The value does not need to be monotonic.
9 // Gauge describes a metric that takes a specific value over time.
10 // An example of a gauge is the current depth of a job queue.
1411 type Gauge interface {
15 Name() string
16 With(Field) Gauge
12 With(labelValues ...string) Gauge
1713 Set(value float64)
18 Add(delta float64)
19 Get() float64
2014 }
2115
22 // Histogram tracks the distribution of a stream of values (e.g. the number of
23 // milliseconds it takes to handle requests). Implementations may choose to
24 // add gauges for values at meaningful quantiles.
16 // Histogram describes a metric that takes repeated observations of the same
17 // kind of thing, and produces a statistical summary of those observations,
18 // typically expressed as quantile buckets. An example of a histogram is HTTP
19 // request latencies.
2520 type Histogram interface {
26 Name() string
27 With(Field) Histogram
28 Observe(value int64)
29 Distribution() ([]Bucket, []Quantile)
21 With(labelValues ...string) Histogram
22 Observe(value float64)
3023 }
31
32 // Field is a key/value pair associated with an observation for a specific
33 // metric. Fields may be ignored by implementations.
34 type Field struct {
35 Key string
36 Value string
37 }
38
39 // Bucket is a range in a histogram which aggregates observations.
40 type Bucket struct {
41 From int64
42 To int64
43 Count int64
44 }
45
46 // Quantile is a pair of quantile (0..100) and its observed maximum value.
47 type Quantile struct {
48 Quantile int // 0..100
49 Value int64
50 }
+0
-112
metrics/multi.go less more
0 package metrics
1
2 type multiCounter struct {
3 name string
4 a []Counter
5 }
6
7 // NewMultiCounter returns a wrapper around multiple Counters.
8 func NewMultiCounter(name string, counters ...Counter) Counter {
9 return &multiCounter{
10 name: name,
11 a: counters,
12 }
13 }
14
15 func (c multiCounter) Name() string { return c.name }
16
17 func (c multiCounter) With(f Field) Counter {
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)
24 }
25 return next
26 }
27
28 func (c multiCounter) Add(delta uint64) {
29 for _, counter := range c.a {
30 counter.Add(delta)
31 }
32 }
33
34 type multiGauge struct {
35 name string
36 a []Gauge
37 }
38
39 func (g multiGauge) Name() string { return g.name }
40
41 // NewMultiGauge returns a wrapper around multiple Gauges.
42 func NewMultiGauge(name string, gauges ...Gauge) Gauge {
43 return &multiGauge{
44 name: name,
45 a: gauges,
46 }
47 }
48
49 func (g multiGauge) With(f Field) Gauge {
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)
56 }
57 return next
58 }
59
60 func (g multiGauge) Set(value float64) {
61 for _, gauge := range g.a {
62 gauge.Set(value)
63 }
64 }
65
66 func (g multiGauge) Add(delta float64) {
67 for _, gauge := range g.a {
68 gauge.Add(delta)
69 }
70 }
71
72 func (g multiGauge) Get() float64 {
73 panic("cannot call Get on a MultiGauge")
74 }
75
76 type multiHistogram struct {
77 name string
78 a []Histogram
79 }
80
81 // NewMultiHistogram returns a wrapper around multiple Histograms.
82 func NewMultiHistogram(name string, histograms ...Histogram) Histogram {
83 return &multiHistogram{
84 name: name,
85 a: histograms,
86 }
87 }
88
89 func (h multiHistogram) Name() string { return h.name }
90
91 func (h multiHistogram) With(f Field) Histogram {
92 next := &multiHistogram{
93 name: h.name,
94 a: make([]Histogram, len(h.a)),
95 }
96 for i, histogram := range h.a {
97 next.a[i] = histogram.With(f)
98 }
99 return next
100 }
101
102 func (h multiHistogram) Observe(value int64) {
103 for _, histogram := range h.a {
104 histogram.Observe(value)
105 }
106 }
107
108 func (h multiHistogram) Distribution() ([]Bucket, []Quantile) {
109 // TODO(pb): there may be a way to do this
110 panic("cannot call Distribution on a MultiHistogram")
111 }
+0
-233
metrics/multi_test.go less more
0 package metrics_test
1
2 import (
3 stdexpvar "expvar"
4 "fmt"
5 "io/ioutil"
6 "math"
7 "net/http"
8 "net/http/httptest"
9 "regexp"
10 "strconv"
11 "strings"
12 "testing"
13
14 stdprometheus "github.com/prometheus/client_golang/prometheus"
15
16 "github.com/go-kit/kit/metrics"
17 "github.com/go-kit/kit/metrics/expvar"
18 "github.com/go-kit/kit/metrics/prometheus"
19 "github.com/go-kit/kit/metrics/teststat"
20 )
21
22 func TestMultiWith(t *testing.T) {
23 c := metrics.NewMultiCounter(
24 "multifoo",
25 expvar.NewCounter("foo"),
26 prometheus.NewCounter(stdprometheus.CounterOpts{
27 Namespace: "test",
28 Subsystem: "multi_with",
29 Name: "bar",
30 Help: "Bar counter.",
31 }, []string{"a"}),
32 )
33
34 c.Add(1)
35 c.With(metrics.Field{Key: "a", Value: "1"}).Add(2)
36 c.Add(3)
37
38 if want, have := strings.Join([]string{
39 `# HELP test_multi_with_bar Bar counter.`,
40 `# TYPE test_multi_with_bar counter`,
41 `test_multi_with_bar{a="1"} 2`,
42 `test_multi_with_bar{a="unknown"} 4`,
43 }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
44 t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
45 }
46 }
47
48 func TestMultiCounter(t *testing.T) {
49 metrics.NewMultiCounter(
50 "multialpha",
51 expvar.NewCounter("alpha"),
52 prometheus.NewCounter(stdprometheus.CounterOpts{
53 Namespace: "test",
54 Subsystem: "multi_counter",
55 Name: "beta",
56 Help: "Beta counter.",
57 }, []string{"a"}),
58 ).With(metrics.Field{Key: "a", Value: "b"}).Add(123)
59
60 if want, have := "123", stdexpvar.Get("alpha").String(); want != have {
61 t.Errorf("expvar: want %q, have %q", want, have)
62 }
63
64 if want, have := strings.Join([]string{
65 `# HELP test_multi_counter_beta Beta counter.`,
66 `# TYPE test_multi_counter_beta counter`,
67 `test_multi_counter_beta{a="b"} 123`,
68 }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
69 t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
70 }
71 }
72
73 func TestMultiGauge(t *testing.T) {
74 g := metrics.NewMultiGauge(
75 "multidelta",
76 expvar.NewGauge("delta"),
77 prometheus.NewGauge(stdprometheus.GaugeOpts{
78 Namespace: "test",
79 Subsystem: "multi_gauge",
80 Name: "kappa",
81 Help: "Kappa gauge.",
82 }, []string{"a"}),
83 )
84
85 f := metrics.Field{Key: "a", Value: "aaa"}
86 g.With(f).Set(34)
87
88 if want, have := "34", stdexpvar.Get("delta").String(); want != have {
89 t.Errorf("expvar: want %q, have %q", want, have)
90 }
91 if want, have := strings.Join([]string{
92 `# HELP test_multi_gauge_kappa Kappa gauge.`,
93 `# TYPE test_multi_gauge_kappa gauge`,
94 `test_multi_gauge_kappa{a="aaa"} 34`,
95 }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
96 t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
97 }
98
99 g.With(f).Add(-40)
100
101 if want, have := "-6", stdexpvar.Get("delta").String(); want != have {
102 t.Errorf("expvar: want %q, have %q", want, have)
103 }
104 if want, have := strings.Join([]string{
105 `# HELP test_multi_gauge_kappa Kappa gauge.`,
106 `# TYPE test_multi_gauge_kappa gauge`,
107 `test_multi_gauge_kappa{a="aaa"} -6`,
108 }, "\n"), scrapePrometheus(t); !strings.Contains(have, want) {
109 t.Errorf("Prometheus metric stanza not found or incorrect\n%s", have)
110 }
111 }
112
113 func TestMultiHistogram(t *testing.T) {
114 quantiles := []int{50, 90, 99}
115 h := metrics.NewMultiHistogram(
116 "multiomicron",
117 expvar.NewHistogram("omicron", 0, 100, 3, quantiles...),
118 prometheus.NewSummary(stdprometheus.SummaryOpts{
119 Namespace: "test",
120 Subsystem: "multi_histogram",
121 Name: "nu",
122 Help: "Nu histogram.",
123 }, []string{}),
124 )
125
126 const seed, mean, stdev int64 = 123, 50, 10
127 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
128 assertExpvarNormalHistogram(t, "omicron", mean, stdev, quantiles)
129 assertPrometheusNormalHistogram(t, `test_multi_histogram_nu`, mean, stdev)
130 }
131
132 func assertExpvarNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) {
133 const tolerance int = 2
134 for _, quantile := range quantiles {
135 want := normalValueAtQuantile(mean, stdev, quantile)
136 s := stdexpvar.Get(fmt.Sprintf("%s_p%02d", metricName, quantile)).String()
137 have, err := strconv.Atoi(s)
138 if err != nil {
139 t.Fatal(err)
140 }
141 if int(math.Abs(float64(want)-float64(have))) > tolerance {
142 t.Errorf("quantile %d: want %d, have %d", quantile, want, have)
143 }
144 }
145 }
146
147 func assertPrometheusNormalHistogram(t *testing.T, metricName string, mean, stdev int64) {
148 scrape := scrapePrometheus(t)
149 const tolerance int = 5 // Prometheus approximates higher quantiles badly -_-;
150 for quantileInt, quantileStr := range map[int]string{50: "0.5", 90: "0.9", 99: "0.99"} {
151 want := normalValueAtQuantile(mean, stdev, quantileInt)
152 have := getPrometheusQuantile(t, scrape, metricName, quantileStr)
153 if int(math.Abs(float64(want)-float64(have))) > tolerance {
154 t.Errorf("%q: want %d, have %d", quantileStr, want, have)
155 }
156 }
157 }
158
159 // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
160 func normalValueAtQuantile(mean, stdev int64, quantile int) int64 {
161 return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1))
162 }
163
164 // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
165 func erfinv(y float64) float64 {
166 if y < -1.0 || y > 1.0 {
167 panic("invalid input")
168 }
169
170 var (
171 a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
172 b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
173 c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
174 d = [2]float64{3.543889200, 1.637067800}
175 )
176
177 const y0 = 0.7
178 var x, z float64
179
180 if math.Abs(y) == 1.0 {
181 x = -y * math.Log(0.0)
182 } else if y < -y0 {
183 z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
184 x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
185 } else {
186 if y < y0 {
187 z = y * y
188 x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0)
189 } else {
190 z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
191 x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
192 }
193 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
194 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
195 }
196
197 return x
198 }
199
200 func scrapePrometheus(t *testing.T) string {
201 server := httptest.NewServer(stdprometheus.UninstrumentedHandler())
202 defer server.Close()
203
204 resp, err := http.Get(server.URL)
205 if err != nil {
206 t.Fatal(err)
207 }
208 defer resp.Body.Close()
209
210 buf, err := ioutil.ReadAll(resp.Body)
211 if err != nil {
212 t.Fatal(err)
213 }
214
215 return strings.TrimSpace(string(buf))
216 }
217
218 func getPrometheusQuantile(t *testing.T, scrape, name, quantileStr string) int {
219 re := name + `{quantile="` + quantileStr + `"} ([0-9]+)`
220 matches := regexp.MustCompile(re).FindAllStringSubmatch(scrape, -1)
221 if len(matches) < 1 {
222 t.Fatalf("%q: quantile %q not found in scrape (%s)", name, quantileStr, re)
223 }
224 if len(matches[0]) < 2 {
225 t.Fatalf("%q: quantile %q not found in scrape (%s)", name, quantileStr, re)
226 }
227 i, err := strconv.Atoi(matches[0][1])
228 if err != nil {
229 t.Fatal(err)
230 }
231 return i
232 }
+0
-42
metrics/print.go less more
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, h Histogram) {
16 buckets, quantiles := h.Distribution()
17
18 fmt.Fprintf(w, "name: %v\n", h.Name())
19 fmt.Fprintf(w, "quantiles: %v\n", quantiles)
20
21 var total float64
22 for _, bucket := range buckets {
23 total += float64(bucket.Count)
24 }
25
26 tw := tabwriter.NewWriter(w, 0, 2, 2, ' ', 0)
27 fmt.Fprintf(tw, "From\tTo\tCount\tProb\tBar\n")
28
29 axis := "|"
30 for _, bucket := range buckets {
31 if bucket.Count > 0 {
32 p := float64(bucket.Count) / total
33 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)])
34 axis = "|"
35 } else {
36 axis = ":" // show that some bars were skipped
37 }
38 }
39
40 tw.Flush()
41 }
+0
-40
metrics/print_test.go less more
0 package metrics_test
1
2 import (
3 "bytes"
4 "testing"
5
6 "math"
7
8 "github.com/go-kit/kit/metrics"
9 "github.com/go-kit/kit/metrics/expvar"
10 "github.com/go-kit/kit/metrics/teststat"
11 )
12
13 func TestPrintDistribution(t *testing.T) {
14 var (
15 quantiles = []int{50, 90, 95, 99}
16 h = expvar.NewHistogram("test_print_distribution", 0, 100, 3, quantiles...)
17 seed = int64(555)
18 mean = int64(5)
19 stdev = int64(1)
20 )
21 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
22
23 var buf bytes.Buffer
24 metrics.PrintDistribution(&buf, h)
25 t.Logf("\n%s\n", buf.String())
26
27 // Count the number of bar chart characters.
28 // We should have ca. 100 in any distribution with a small-enough stdev.
29
30 var n int
31 for _, r := range buf.String() {
32 if r == '#' {
33 n++
34 }
35 }
36 if want, have, tol := 100, n, 5; int(math.Abs(float64(want-have))) > tol {
37 t.Errorf("want %d, have %d (tolerance %d)", want, have, tol)
38 }
39 }
0 // Package prometheus implements a Prometheus backend for package metrics.
0 // Package prometheus provides Prometheus implementations for metrics.
1 // Individual metrics are mapped to their Prometheus counterparts, and
2 // (depending on the constructor used) may be automatically registered in the
3 // global Prometheus metrics registry.
14 package prometheus
25
36 import (
47 "github.com/prometheus/client_golang/prometheus"
58
6 "github.com/go-kit/kit/metrics"
9 "github.com/go-kit/kit/metrics3"
10 "github.com/go-kit/kit/metrics3/internal/lv"
711 )
812
9 // Prometheus has strong opinions about the dimensionality of fields. Users
10 // must predeclare every field key they intend to use. On every observation,
11 // fields with keys that haven't been predeclared will be silently dropped,
12 // and predeclared field keys without values will receive the value
13 // PrometheusLabelValueUnknown.
14 var PrometheusLabelValueUnknown = "unknown"
15
16 type counter struct {
17 *prometheus.CounterVec
18 name string
19 Pairs map[string]string
13 // Counter implements Counter, via a Prometheus CounterVec.
14 type Counter struct {
15 cv *prometheus.CounterVec
16 lvs lv.LabelValues
2017 }
2118
22 // NewCounter returns a new Counter backed by a Prometheus metric. The counter
23 // is automatically registered via prometheus.Register.
24 func NewCounter(opts prometheus.CounterOpts, fieldKeys []string) metrics.Counter {
25 m := prometheus.NewCounterVec(opts, fieldKeys)
26 prometheus.MustRegister(m)
27 p := map[string]string{}
28 for _, fieldName := range fieldKeys {
29 p[fieldName] = PrometheusLabelValueUnknown
30 }
31 return counter{
32 CounterVec: m,
33 name: opts.Name,
34 Pairs: p,
19 // NewCounterFrom constructs and registers a Prometheus CounterVec,
20 // and returns a usable Counter object.
21 func NewCounterFrom(opts prometheus.CounterOpts, labelNames []string) *Counter {
22 cv := prometheus.NewCounterVec(opts, labelNames)
23 prometheus.MustRegister(cv)
24 return NewCounter(cv)
25 }
26
27 // NewCounter wraps the CounterVec and returns a usable Counter object.
28 func NewCounter(cv *prometheus.CounterVec) *Counter {
29 return &Counter{
30 cv: cv,
3531 }
3632 }
3733
38 func (c counter) Name() string { return c.name }
39
40 func (c counter) With(f metrics.Field) metrics.Counter {
41 return counter{
42 CounterVec: c.CounterVec,
43 name: c.name,
44 Pairs: merge(c.Pairs, f),
34 // With implements Counter.
35 func (c *Counter) With(labelValues ...string) metrics.Counter {
36 return &Counter{
37 cv: c.cv,
38 lvs: c.lvs.With(labelValues...),
4539 }
4640 }
4741
48 func (c counter) Add(delta uint64) {
49 c.CounterVec.With(prometheus.Labels(c.Pairs)).Add(float64(delta))
42 // Add implements Counter.
43 func (c *Counter) Add(delta float64) {
44 c.cv.WithLabelValues(c.lvs...).Add(delta)
5045 }
5146
52 type gauge struct {
53 *prometheus.GaugeVec
54 name string
55 Pairs map[string]string
47 // Gauge implements Gauge, via a Prometheus GaugeVec.
48 type Gauge struct {
49 gv *prometheus.GaugeVec
50 lvs lv.LabelValues
5651 }
5752
58 // NewGauge returns a new Gauge backed by a Prometheus metric. The gauge is
59 // automatically registered via prometheus.Register.
60 func NewGauge(opts prometheus.GaugeOpts, fieldKeys []string) metrics.Gauge {
61 m := prometheus.NewGaugeVec(opts, fieldKeys)
62 prometheus.MustRegister(m)
63 return gauge{
64 GaugeVec: m,
65 name: opts.Name,
66 Pairs: pairsFrom(fieldKeys),
53 // NewGaugeFrom construts and registers a Prometheus GaugeVec,
54 // and returns a usable Gauge object.
55 func NewGaugeFrom(opts prometheus.GaugeOpts, labelNames []string) *Gauge {
56 gv := prometheus.NewGaugeVec(opts, labelNames)
57 prometheus.MustRegister(gv)
58 return NewGauge(gv)
59 }
60
61 // NewGauge wraps the GaugeVec and returns a usable Gauge object.
62 func NewGauge(gv *prometheus.GaugeVec) *Gauge {
63 return &Gauge{
64 gv: gv,
6765 }
6866 }
6967
70 func (g gauge) Name() string { return g.name }
71
72 func (g gauge) With(f metrics.Field) metrics.Gauge {
73 return gauge{
74 GaugeVec: g.GaugeVec,
75 name: g.name,
76 Pairs: merge(g.Pairs, f),
68 // With implements Gauge.
69 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
70 return &Gauge{
71 gv: g.gv,
72 lvs: g.lvs.With(labelValues...),
7773 }
7874 }
7975
80 func (g gauge) Set(value float64) {
81 g.GaugeVec.With(prometheus.Labels(g.Pairs)).Set(value)
76 // Set implements Gauge.
77 func (g *Gauge) Set(value float64) {
78 g.gv.WithLabelValues(g.lvs...).Set(value)
8279 }
8380
84 func (g gauge) Add(delta float64) {
85 g.GaugeVec.With(prometheus.Labels(g.Pairs)).Add(delta)
81 // Add is supported by Prometheus GaugeVecs.
82 func (g *Gauge) Add(delta float64) {
83 g.gv.WithLabelValues(g.lvs...).Add(delta)
8684 }
8785
88 func (g gauge) Get() float64 {
89 // TODO(pb): see https://github.com/prometheus/client_golang/issues/58
90 return 0.0
86 // Summary implements Histogram, via a Prometheus SummaryVec. The difference
87 // between a Summary and a Histogram is that Summaries don't require predefined
88 // quantile buckets, but cannot be statistically aggregated.
89 type Summary struct {
90 sv *prometheus.SummaryVec
91 lvs lv.LabelValues
9192 }
9293
93 // RegisterCallbackGauge registers a Gauge with Prometheus whose value is
94 // determined at collect time by the passed callback function. The callback
95 // determines the value, and fields are ignored, so RegisterCallbackGauge
96 // returns nothing.
97 func RegisterCallbackGauge(opts prometheus.GaugeOpts, callback func() float64) {
98 prometheus.MustRegister(prometheus.NewGaugeFunc(opts, callback))
94 // NewSummaryFrom constructs and registers a Prometheus SummaryVec,
95 // and returns a usable Summary object.
96 func NewSummaryFrom(opts prometheus.SummaryOpts, labelNames []string) *Summary {
97 sv := prometheus.NewSummaryVec(opts, labelNames)
98 prometheus.MustRegister(sv)
99 return NewSummary(sv)
99100 }
100101
101 type summary struct {
102 *prometheus.SummaryVec
103 name string
104 Pairs map[string]string
105 }
106
107 // NewSummary returns a new Histogram backed by a Prometheus summary. The
108 // histogram is automatically registered via prometheus.Register.
109 //
110 // For more information on Prometheus histograms and summaries, refer to
111 // http://prometheus.io/docs/practices/histograms.
112 func NewSummary(opts prometheus.SummaryOpts, fieldKeys []string) metrics.Histogram {
113 m := prometheus.NewSummaryVec(opts, fieldKeys)
114 prometheus.MustRegister(m)
115 return summary{
116 SummaryVec: m,
117 name: opts.Name,
118 Pairs: pairsFrom(fieldKeys),
102 // NewSummary wraps the SummaryVec and returns a usable Summary object.
103 func NewSummary(sv *prometheus.SummaryVec) *Summary {
104 return &Summary{
105 sv: sv,
119106 }
120107 }
121108
122 func (s summary) Name() string { return s.name }
123
124 func (s summary) With(f metrics.Field) metrics.Histogram {
125 return summary{
126 SummaryVec: s.SummaryVec,
127 name: s.name,
128 Pairs: merge(s.Pairs, f),
109 // With implements Histogram.
110 func (s *Summary) With(labelValues ...string) metrics.Histogram {
111 return &Summary{
112 sv: s.sv,
113 lvs: s.lvs.With(labelValues...),
129114 }
130115 }
131116
132 func (s summary) Observe(value int64) {
133 s.SummaryVec.With(prometheus.Labels(s.Pairs)).Observe(float64(value))
117 // Observe implements Histogram.
118 func (s *Summary) Observe(value float64) {
119 s.sv.WithLabelValues(s.lvs...).Observe(value)
134120 }
135121
136 func (s summary) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
137 // TODO(pb): see https://github.com/prometheus/client_golang/issues/58
138 return []metrics.Bucket{}, []metrics.Quantile{}
122 // Histogram implements Histogram via a Prometheus HistogramVec. The difference
123 // between a Histogram and a Summary is that Histograms require predefined
124 // quantile buckets, and can be statistically aggregated.
125 type Histogram struct {
126 hv *prometheus.HistogramVec
127 lvs lv.LabelValues
139128 }
140129
141 type histogram struct {
142 *prometheus.HistogramVec
143 name string
144 Pairs map[string]string
130 // NewHistogramFrom constructs and registers a Prometheus HistogramVec,
131 // and returns a usable Histogram object.
132 func NewHistogramFrom(opts prometheus.HistogramOpts, labelNames []string) *Histogram {
133 hv := prometheus.NewHistogramVec(opts, labelNames)
134 prometheus.MustRegister(hv)
135 return NewHistogram(hv)
145136 }
146137
147 // NewHistogram returns a new Histogram backed by a Prometheus Histogram. The
148 // histogram is automatically registered via prometheus.Register.
149 //
150 // For more information on Prometheus histograms and summaries, refer to
151 // http://prometheus.io/docs/practices/histograms.
152 func NewHistogram(opts prometheus.HistogramOpts, fieldKeys []string) metrics.Histogram {
153 m := prometheus.NewHistogramVec(opts, fieldKeys)
154 prometheus.MustRegister(m)
155 return histogram{
156 HistogramVec: m,
157 name: opts.Name,
158 Pairs: pairsFrom(fieldKeys),
138 // NewHistogram wraps the HistogramVec and returns a usable Histogram object.
139 func NewHistogram(hv *prometheus.HistogramVec) *Histogram {
140 return &Histogram{
141 hv: hv,
159142 }
160143 }
161144
162 func (h histogram) Name() string { return h.name }
163
164 func (h histogram) With(f metrics.Field) metrics.Histogram {
165 return histogram{
166 HistogramVec: h.HistogramVec,
167 name: h.name,
168 Pairs: merge(h.Pairs, f),
145 // With implements Histogram.
146 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
147 return &Histogram{
148 hv: h.hv,
149 lvs: h.lvs.With(labelValues...),
169150 }
170151 }
171152
172 func (h histogram) Observe(value int64) {
173 h.HistogramVec.With(prometheus.Labels(h.Pairs)).Observe(float64(value))
153 // Observe implements Histogram.
154 func (h *Histogram) Observe(value float64) {
155 h.hv.WithLabelValues(h.lvs...).Observe(value)
174156 }
175
176 func (h histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
177 // TODO(pb): see https://github.com/prometheus/client_golang/issues/58
178 return []metrics.Bucket{}, []metrics.Quantile{}
179 }
180
181 func pairsFrom(fieldKeys []string) map[string]string {
182 p := map[string]string{}
183 for _, fieldName := range fieldKeys {
184 p[fieldName] = PrometheusLabelValueUnknown
185 }
186 return p
187 }
188
189 func merge(orig map[string]string, f metrics.Field) map[string]string {
190 if _, ok := orig[f.Key]; !ok {
191 return orig
192 }
193
194 newPairs := make(map[string]string, len(orig))
195 for k, v := range orig {
196 newPairs[k] = v
197 }
198
199 newPairs[f.Key] = f.Value
200 return newPairs
201 }
0 package prometheus_test
0 package prometheus
11
22 import (
3 "io/ioutil"
4 "math"
5 "math/rand"
6 "net/http"
7 "net/http/httptest"
8 "regexp"
9 "strconv"
310 "strings"
411 "testing"
512
13 "github.com/go-kit/kit/metrics3/teststat"
614 stdprometheus "github.com/prometheus/client_golang/prometheus"
7
8 "github.com/go-kit/kit/metrics"
9 "github.com/go-kit/kit/metrics/prometheus"
10 "github.com/go-kit/kit/metrics/teststat"
1115 )
1216
13 func TestPrometheusLabelBehavior(t *testing.T) {
14 c := prometheus.NewCounter(stdprometheus.CounterOpts{
15 Namespace: "test",
16 Subsystem: "prometheus_label_behavior",
17 Name: "foobar",
18 Help: "Abc def.",
19 }, []string{"used_key", "unused_key"})
20 c.With(metrics.Field{Key: "used_key", Value: "declared"}).Add(1)
21 c.Add(1)
17 func TestCounter(t *testing.T) {
18 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
19 defer s.Close()
2220
23 if want, have := strings.Join([]string{
24 `# HELP test_prometheus_label_behavior_foobar Abc def.`,
25 `# TYPE test_prometheus_label_behavior_foobar counter`,
26 `test_prometheus_label_behavior_foobar{unused_key="unknown",used_key="declared"} 1`,
27 `test_prometheus_label_behavior_foobar{unused_key="unknown",used_key="unknown"} 1`,
28 }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) {
29 t.Errorf("metric stanza not found or incorrect\n%s", have)
21 scrape := func() string {
22 resp, _ := http.Get(s.URL)
23 buf, _ := ioutil.ReadAll(resp.Body)
24 return string(buf)
25 }
26
27 namespace, subsystem, name := "ns", "ss", "foo"
28 re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + ` ([0-9\.]+)`)
29
30 counter := NewCounterFrom(stdprometheus.CounterOpts{
31 Namespace: namespace,
32 Subsystem: subsystem,
33 Name: name,
34 Help: "This is the help string.",
35 }, []string{})
36
37 value := func() float64 {
38 matches := re.FindStringSubmatch(scrape())
39 f, _ := strconv.ParseFloat(matches[1], 64)
40 return f
41 }
42
43 if err := teststat.TestCounter(counter, value); err != nil {
44 t.Fatal(err)
3045 }
3146 }
3247
33 func TestPrometheusCounter(t *testing.T) {
34 c := prometheus.NewCounter(stdprometheus.CounterOpts{
35 Namespace: "test",
36 Subsystem: "prometheus_counter",
37 Name: "foobar",
38 Help: "Lorem ipsum.",
48 func TestGauge(t *testing.T) {
49 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
50 defer s.Close()
51
52 scrape := func() string {
53 resp, _ := http.Get(s.URL)
54 buf, _ := ioutil.ReadAll(resp.Body)
55 return string(buf)
56 }
57
58 namespace, subsystem, name := "aaa", "bbb", "ccc"
59 re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + ` ([0-9\.]+)`)
60
61 gauge := NewGaugeFrom(stdprometheus.GaugeOpts{
62 Namespace: namespace,
63 Subsystem: subsystem,
64 Name: name,
65 Help: "This is a different help string.",
3966 }, []string{})
40 c.Add(1)
41 c.Add(2)
42 if want, have := strings.Join([]string{
43 `# HELP test_prometheus_counter_foobar Lorem ipsum.`,
44 `# TYPE test_prometheus_counter_foobar counter`,
45 `test_prometheus_counter_foobar 3`,
46 }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) {
47 t.Errorf("metric stanza not found or incorrect\n%s", have)
67
68 value := func() float64 {
69 matches := re.FindStringSubmatch(scrape())
70 f, _ := strconv.ParseFloat(matches[1], 64)
71 return f
4872 }
49 c.Add(3)
50 c.Add(4)
51 if want, have := strings.Join([]string{
52 `# HELP test_prometheus_counter_foobar Lorem ipsum.`,
53 `# TYPE test_prometheus_counter_foobar counter`,
54 `test_prometheus_counter_foobar 10`,
55 }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) {
56 t.Errorf("metric stanza not found or incorrect\n%s", have)
73
74 if err := teststat.TestGauge(gauge, value); err != nil {
75 t.Fatal(err)
5776 }
5877 }
5978
60 func TestPrometheusGauge(t *testing.T) {
61 c := prometheus.NewGauge(stdprometheus.GaugeOpts{
62 Namespace: "test",
63 Subsystem: "prometheus_gauge",
64 Name: "foobar",
65 Help: "Dolor sit.",
79 func TestSummary(t *testing.T) {
80 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
81 defer s.Close()
82
83 scrape := func() string {
84 resp, _ := http.Get(s.URL)
85 buf, _ := ioutil.ReadAll(resp.Body)
86 return string(buf)
87 }
88
89 namespace, subsystem, name := "test", "prometheus", "summary"
90 re50 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{quantile="0.5"} ([0-9\.]+)`)
91 re90 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{quantile="0.9"} ([0-9\.]+)`)
92 re99 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{quantile="0.99"} ([0-9\.]+)`)
93
94 summary := NewSummaryFrom(stdprometheus.SummaryOpts{
95 Namespace: namespace,
96 Subsystem: subsystem,
97 Name: name,
98 Help: "This is the help string for the summary.",
6699 }, []string{})
67 c.Set(42)
68 if want, have := strings.Join([]string{
69 `# HELP test_prometheus_gauge_foobar Dolor sit.`,
70 `# TYPE test_prometheus_gauge_foobar gauge`,
71 `test_prometheus_gauge_foobar 42`,
72 }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) {
73 t.Errorf("metric stanza not found or incorrect\n%s", have)
100
101 quantiles := func() (float64, float64, float64, float64) {
102 buf := scrape()
103 match50 := re50.FindStringSubmatch(buf)
104 p50, _ := strconv.ParseFloat(match50[1], 64)
105 match90 := re90.FindStringSubmatch(buf)
106 p90, _ := strconv.ParseFloat(match90[1], 64)
107 match99 := re99.FindStringSubmatch(buf)
108 p99, _ := strconv.ParseFloat(match99[1], 64)
109 p95 := p90 + ((p99 - p90) / 2) // Prometheus, y u no p95??? :< #yolo
110 return p50, p90, p95, p99
74111 }
75 c.Add(-43)
76 if want, have := strings.Join([]string{
77 `# HELP test_prometheus_gauge_foobar Dolor sit.`,
78 `# TYPE test_prometheus_gauge_foobar gauge`,
79 `test_prometheus_gauge_foobar -1`,
80 }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) {
81 t.Errorf("metric stanza not found or incorrect\n%s", have)
112
113 if err := teststat.TestHistogram(summary, quantiles, 0.01); err != nil {
114 t.Fatal(err)
82115 }
83116 }
84117
85 func TestPrometheusCallbackGauge(t *testing.T) {
86 value := 123.456
87 cb := func() float64 { return value }
88 prometheus.RegisterCallbackGauge(stdprometheus.GaugeOpts{
89 Namespace: "test",
90 Subsystem: "prometheus_gauge",
91 Name: "bazbaz",
92 Help: "Help string.",
93 }, cb)
94 if want, have := strings.Join([]string{
95 `# HELP test_prometheus_gauge_bazbaz Help string.`,
96 `# TYPE test_prometheus_gauge_bazbaz gauge`,
97 `test_prometheus_gauge_bazbaz 123.456`,
98 }, "\n"), teststat.ScrapePrometheus(t); !strings.Contains(have, want) {
99 t.Errorf("metric stanza not found or incorrect\n%s", have)
118 func TestHistogram(t *testing.T) {
119 // Prometheus reports histograms as a count of observations that fell into
120 // each predefined bucket, with the bucket value representing a global upper
121 // limit. That is, the count monotonically increases over the buckets. This
122 // requires a different strategy to test.
123
124 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
125 defer s.Close()
126
127 scrape := func() string {
128 resp, _ := http.Get(s.URL)
129 buf, _ := ioutil.ReadAll(resp.Body)
130 return string(buf)
131 }
132
133 namespace, subsystem, name := "test", "prometheus", "histogram"
134 re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `_bucket{le="([0-9]+|\+Inf)"} ([0-9\.]+)`)
135
136 numStdev := 3
137 bucketMin := (teststat.Mean - (numStdev * teststat.Stdev))
138 bucketMax := (teststat.Mean + (numStdev * teststat.Stdev))
139 if bucketMin < 0 {
140 bucketMin = 0
141 }
142 bucketCount := 10
143 bucketDelta := (bucketMax - bucketMin) / bucketCount
144 buckets := []float64{}
145 for i := bucketMin; i <= bucketMax; i += bucketDelta {
146 buckets = append(buckets, float64(i))
147 }
148
149 histogram := NewHistogramFrom(stdprometheus.HistogramOpts{
150 Namespace: namespace,
151 Subsystem: subsystem,
152 Name: name,
153 Help: "This is the help string for the histogram.",
154 Buckets: buckets,
155 }, []string{})
156
157 // Can't TestHistogram, because Prometheus Histograms don't dynamically
158 // compute quantiles. Instead, they fill up buckets. So, let's populate the
159 // histogram kind of manually.
160 teststat.PopulateNormalHistogram(histogram, rand.Int())
161
162 // Then, we use ExpectedObservationsLessThan to validate.
163 for _, line := range strings.Split(scrape(), "\n") {
164 match := re.FindStringSubmatch(line)
165 if match == nil {
166 continue
167 }
168
169 bucket, _ := strconv.ParseInt(match[1], 10, 64)
170 have, _ := strconv.ParseInt(match[2], 10, 64)
171
172 want := teststat.ExpectedObservationsLessThan(bucket)
173 if match[1] == "+Inf" {
174 want = int64(teststat.Count) // special case
175 }
176
177 // Unfortunately, we observe experimentally that Prometheus is quite
178 // imprecise at the extremes. I'm setting a very high tolerance for now.
179 // It would be great to dig in and figure out whether that's a problem
180 // with my Expected calculation, or in Prometheus.
181 tolerance := 0.25
182 if delta := math.Abs(float64(want) - float64(have)); (delta / float64(want)) > tolerance {
183 t.Errorf("Bucket %d: want %d, have %d (%.1f%%)", bucket, want, have, (100.0 * delta / float64(want)))
184 }
100185 }
101186 }
102187
103 func TestPrometheusSummary(t *testing.T) {
104 h := prometheus.NewSummary(stdprometheus.SummaryOpts{
105 Namespace: "test",
106 Subsystem: "prometheus_summary_histogram",
107 Name: "foobar",
108 Help: "Qwerty asdf.",
109 }, []string{})
110
111 const mean, stdev int64 = 50, 10
112 teststat.PopulateNormalHistogram(t, h, 34, mean, stdev)
113 teststat.AssertPrometheusNormalSummary(t, "test_prometheus_summary_histogram_foobar", mean, stdev)
188 func TestWith(t *testing.T) {
189 t.Skip("TODO")
114190 }
115
116 func TestPrometheusHistogram(t *testing.T) {
117 buckets := []float64{20, 40, 60, 80, 100}
118 h := prometheus.NewHistogram(stdprometheus.HistogramOpts{
119 Namespace: "test",
120 Subsystem: "prometheus_histogram_histogram",
121 Name: "quux",
122 Help: "Qwerty asdf.",
123 Buckets: buckets,
124 }, []string{})
125
126 const mean, stdev int64 = 50, 10
127 teststat.PopulateNormalHistogram(t, h, 34, mean, stdev)
128 teststat.AssertPrometheusBucketedHistogram(t, "test_prometheus_histogram_histogram_quux_bucket", mean, stdev, buckets)
129 }
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/circonus"
5 )
6
7 type circonusProvider struct {
8 c *circonus.Circonus
9 }
10
11 // NewCirconusProvider takes the given Circonnus object and returns a Provider
12 // that produces Circonus metrics.
13 func NewCirconusProvider(c *circonus.Circonus) Provider {
14 return &circonusProvider{
15 c: c,
16 }
17 }
18
19 // NewCounter implements Provider.
20 func (p *circonusProvider) NewCounter(name string) metrics.Counter {
21 return p.c.NewCounter(name)
22 }
23
24 // NewGauge implements Provider.
25 func (p *circonusProvider) NewGauge(name string) metrics.Gauge {
26 return p.c.NewGauge(name)
27 }
28
29 // NewHistogram implements Provider. The buckets parameter is ignored.
30 func (p *circonusProvider) NewHistogram(name string, _ int) metrics.Histogram {
31 return p.c.NewHistogram(name)
32 }
33
34 // Stop implements Provider, but is a no-op.
35 func (p *circonusProvider) Stop() {}
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/discard"
5 )
6
7 type discardProvider struct{}
8
9 // NewDiscardProvider returns a provider that produces no-op metrics via the
10 // discarding backend.
11 func NewDiscardProvider() Provider { return discardProvider{} }
12
13 // NewCounter implements Provider.
14 func (discardProvider) NewCounter(string) metrics.Counter { return discard.NewCounter() }
15
16 // NewGauge implements Provider.
17 func (discardProvider) NewGauge(string) metrics.Gauge { return discard.NewGauge() }
18
19 // NewHistogram implements Provider.
20 func (discardProvider) NewHistogram(string, int) metrics.Histogram { return discard.NewHistogram() }
21
22 // Stop implements Provider.
23 func (discardProvider) Stop() {}
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/dogstatsd"
5 )
6
7 type dogstatsdProvider struct {
8 d *dogstatsd.Dogstatsd
9 stop func()
10 }
11
12 // NewDogstatsdProvider wraps the given Dogstatsd object and stop func and
13 // returns a Provider that produces Dogstatsd metrics. A typical stop function
14 // would be ticker.Stop from the ticker passed to the SendLoop helper method.
15 func NewDogstatsdProvider(d *dogstatsd.Dogstatsd, stop func()) Provider {
16 return &dogstatsdProvider{
17 d: d,
18 stop: stop,
19 }
20 }
21
22 // NewCounter implements Provider, returning a new Dogstatsd Counter with a
23 // sample rate of 1.0.
24 func (p *dogstatsdProvider) NewCounter(name string) metrics.Counter {
25 return p.d.NewCounter(name, 1.0)
26 }
27
28 // NewGauge implements Provider.
29 func (p *dogstatsdProvider) NewGauge(name string) metrics.Gauge {
30 return p.d.NewGauge(name)
31 }
32
33 // NewHistogram implements Provider, returning a new Dogstatsd Histogram (note:
34 // not a Timing) with a sample rate of 1.0. The buckets argument is ignored.
35 func (p *dogstatsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
36 return p.d.NewHistogram(name, 1.0)
37 }
38
39 // Stop implements Provider, invoking the stop function passed at construction.
40 func (p *dogstatsdProvider) Stop() {
41 p.stop()
42 }
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/expvar"
5 )
6
7 type expvarProvider struct{}
8
9 // NewExpvarProvider returns a Provider that produces expvar metrics.
10 func NewExpvarProvider() Provider {
11 return expvarProvider{}
12 }
13
14 // NewCounter implements Provider.
15 func (p expvarProvider) NewCounter(name string) metrics.Counter {
16 return expvar.NewCounter(name)
17 }
18
19 // NewGauge implements Provider.
20 func (p expvarProvider) NewGauge(name string) metrics.Gauge {
21 return expvar.NewGauge(name)
22 }
23
24 // NewHistogram implements Provider.
25 func (p expvarProvider) NewHistogram(name string, buckets int) metrics.Histogram {
26 return expvar.NewHistogram(name, buckets)
27 }
28
29 // Stop implements Provider, but is a no-op.
30 func (p expvarProvider) Stop() {}
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/graphite"
5 )
6
7 type graphiteProvider struct {
8 g *graphite.Graphite
9 stop func()
10 }
11
12 // NewGraphiteProvider wraps the given Graphite object and stop func and returns
13 // a Provider that produces Graphite metrics. A typical stop function would be
14 // ticker.Stop from the ticker passed to the SendLoop helper method.
15 func NewGraphiteProvider(g *graphite.Graphite, stop func()) Provider {
16 return &graphiteProvider{
17 g: g,
18 stop: stop,
19 }
20 }
21
22 // NewCounter implements Provider.
23 func (p *graphiteProvider) NewCounter(name string) metrics.Counter {
24 return p.g.NewCounter(name)
25 }
26
27 // NewGauge implements Provider.
28 func (p *graphiteProvider) NewGauge(name string) metrics.Gauge {
29 return p.g.NewGauge(name)
30 }
31
32 // NewHistogram implements Provider.
33 func (p *graphiteProvider) NewHistogram(name string, buckets int) metrics.Histogram {
34 return p.g.NewHistogram(name, buckets)
35 }
36
37 // Stop implements Provider, invoking the stop function passed at construction.
38 func (p *graphiteProvider) Stop() {
39 p.stop()
40 }
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/influx"
5 )
6
7 type influxProvider struct {
8 in *influx.Influx
9 stop func()
10 }
11
12 // NewInfluxProvider takes the given Influx object and stop func, and returns
13 // a Provider that produces Influx metrics.
14 func NewInfluxProvider(in *influx.Influx, stop func()) Provider {
15 return &influxProvider{
16 in: in,
17 stop: stop,
18 }
19 }
20
21 // NewCounter implements Provider. Per-metric tags are not supported.
22 func (p *influxProvider) NewCounter(name string) metrics.Counter {
23 return p.in.NewCounter(name)
24 }
25
26 // NewGauge implements Provider. Per-metric tags are not supported.
27 func (p *influxProvider) NewGauge(name string) metrics.Gauge {
28 return p.in.NewGauge(name)
29 }
30
31 // NewHistogram implements Provider. Per-metric tags are not supported.
32 func (p *influxProvider) NewHistogram(name string, buckets int) metrics.Histogram {
33 return p.in.NewHistogram(name)
34 }
35
36 // Stop implements Provider, invoking the stop function passed at construction.
37 func (p *influxProvider) Stop() {
38 p.stop()
39 }
0 package provider
1
2 import (
3 stdprometheus "github.com/prometheus/client_golang/prometheus"
4
5 "github.com/go-kit/kit/metrics3"
6 "github.com/go-kit/kit/metrics3/prometheus"
7 )
8
9 type prometheusProvider struct {
10 namespace string
11 subsystem string
12 }
13
14 // NewPrometheusProvider returns a Provider that produces Prometheus metrics.
15 // Namespace and subsystem are applied to all produced metrics.
16 func NewPrometheusProvider(namespace, subsystem string) Provider {
17 return &prometheusProvider{
18 namespace: namespace,
19 subsystem: subsystem,
20 }
21 }
22
23 // NewCounter implements Provider via prometheus.NewCounterFrom, i.e. the
24 // counter is registered. The metric's namespace and subsystem are taken from
25 // the Provider. Help is set to the name of the metric, and no const label names
26 // are set.
27 func (p *prometheusProvider) NewCounter(name string) metrics.Counter {
28 return prometheus.NewCounterFrom(stdprometheus.CounterOpts{
29 Namespace: p.namespace,
30 Subsystem: p.subsystem,
31 Name: name,
32 Help: name,
33 }, []string{})
34 }
35
36 // NewGauge implements Provider via prometheus.NewGaugeFrom, i.e. the gauge is
37 // registered. The metric's namespace and subsystem are taken from the Provider.
38 // Help is set to the name of the metric, and no const label names are set.
39 func (p *prometheusProvider) NewGauge(name string) metrics.Gauge {
40 return prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
41 Namespace: p.namespace,
42 Subsystem: p.subsystem,
43 Name: name,
44 Help: name,
45 }, []string{})
46 }
47
48 // NewGauge implements Provider via prometheus.NewSummaryFrom, i.e. the summary
49 // is registered. The metric's namespace and subsystem are taken from the
50 // Provider. Help is set to the name of the metric, and no const label names are
51 // set. Buckets are ignored.
52 func (p *prometheusProvider) NewHistogram(name string, _ int) metrics.Histogram {
53 return prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
54 Namespace: p.namespace,
55 Subsystem: p.subsystem,
56 Name: name,
57 Help: name,
58 }, []string{})
59 }
60
61 // Stop implements Provider, but is a no-op.
62 func (p *prometheusProvider) Stop() {}
0 // Package provider provides a factory-like abstraction for metrics backends.
1 // This package is provided specifically for the needs of the NY Times framework
2 // Gizmo. Most normal Go kit users shouldn't need to use it.
3 //
4 // Normally, if your microservice needs to support different metrics backends,
5 // you can simply do different construction based on a flag. For example,
6 //
7 // var latency metrics.Histogram
8 // var requests metrics.Counter
9 // switch *metricsBackend {
10 // case "prometheus":
11 // latency = prometheus.NewSummaryVec(...)
12 // requests = prometheus.NewCounterVec(...)
13 // case "statsd":
14 // s := statsd.New(...)
15 // t := time.NewTicker(5*time.Second)
16 // go s.SendLoop(t.C, "tcp", "statsd.local:8125")
17 // latency = s.NewHistogram(...)
18 // requests = s.NewCounter(...)
19 // default:
20 // log.Fatal("unsupported metrics backend %q", *metricsBackend)
21 // }
22 //
23 package provider
24
25 import (
26 "github.com/go-kit/kit/metrics3"
27 )
28
29 // Provider abstracts over constructors and lifecycle management functions for
30 // each supported metrics backend. It should only be used by those who need to
31 // swap out implementations dynamically.
32 //
33 // This is primarily useful for intermediating frameworks, and is likely
34 // unnecessary for most Go kit services. See the package-level doc comment for
35 // more typical usage instructions.
36 type Provider interface {
37 NewCounter(name string) metrics.Counter
38 NewGauge(name string) metrics.Gauge
39 NewHistogram(name string, buckets int) metrics.Histogram
40 Stop()
41 }
+0
-259
metrics/provider/providers.go less more
0 package provider
1
2 import (
3 "errors"
4 "time"
5
6 "github.com/prometheus/client_golang/prometheus"
7
8 "github.com/go-kit/kit/log"
9 "github.com/go-kit/kit/metrics"
10 "github.com/go-kit/kit/metrics/discard"
11 "github.com/go-kit/kit/metrics/dogstatsd"
12 kitexp "github.com/go-kit/kit/metrics/expvar"
13 "github.com/go-kit/kit/metrics/graphite"
14 kitprom "github.com/go-kit/kit/metrics/prometheus"
15 "github.com/go-kit/kit/metrics/statsd"
16 )
17
18 // Provider represents a union set of constructors and lifecycle management
19 // functions for each supported metrics backend. It should be used by those who
20 // need to easily swap out implementations, e.g. dynamically, or at a single
21 // point in an intermediating framework.
22 type Provider interface {
23 NewCounter(name, help string) metrics.Counter
24 NewHistogram(name, help string, min, max int64, sigfigs int, quantiles ...int) (metrics.Histogram, error)
25 NewGauge(name, help string) metrics.Gauge
26 Stop()
27 }
28
29 // NewGraphiteProvider will return a Provider implementation that is a simple
30 // wrapper around a graphite.Emitter. All metric names will be prefixed with the
31 // given value and data will be emitted once every interval. If no network value
32 // is given, it will default to "udp".
33 func NewGraphiteProvider(network, address, prefix string, interval time.Duration, logger log.Logger) (Provider, error) {
34 if network == "" {
35 network = "udp"
36 }
37 if address == "" {
38 return nil, errors.New("address is required")
39 }
40 return graphiteProvider{
41 e: graphite.NewEmitter(network, address, prefix, interval, logger),
42 }, nil
43 }
44
45 type graphiteProvider struct {
46 e *graphite.Emitter
47 }
48
49 var _ Provider = graphiteProvider{}
50
51 // NewCounter implements Provider. Help is ignored.
52 func (p graphiteProvider) NewCounter(name, _ string) metrics.Counter {
53 return p.e.NewCounter(name)
54 }
55
56 // NewHistogram implements Provider. Help is ignored.
57 func (p graphiteProvider) NewHistogram(name, _ string, min, max int64, sigfigs int, quantiles ...int) (metrics.Histogram, error) {
58 return p.e.NewHistogram(name, min, max, sigfigs, quantiles...)
59 }
60
61 // NewGauge implements Provider. Help is ignored.
62 func (p graphiteProvider) NewGauge(name, _ string) metrics.Gauge {
63 return p.e.NewGauge(name)
64 }
65
66 // Stop implements Provider.
67 func (p graphiteProvider) Stop() {
68 p.e.Stop()
69 }
70
71 // NewStatsdProvider will return a Provider implementation that is a simple
72 // wrapper around a statsd.Emitter. All metric names will be prefixed with the
73 // given value and data will be emitted once every interval or when the buffer
74 // has reached its max size. If no network value is given, it will default to
75 // "udp".
76 func NewStatsdProvider(network, address, prefix string, interval time.Duration, logger log.Logger) (Provider, error) {
77 if network == "" {
78 network = "udp"
79 }
80 if address == "" {
81 return nil, errors.New("address is required")
82 }
83 return statsdProvider{
84 e: statsd.NewEmitter(network, address, prefix, interval, logger),
85 }, nil
86 }
87
88 type statsdProvider struct {
89 e *statsd.Emitter
90 }
91
92 var _ Provider = statsdProvider{}
93
94 // NewCounter implements Provider. Help is ignored.
95 func (p statsdProvider) NewCounter(name, _ string) metrics.Counter {
96 return p.e.NewCounter(name)
97 }
98
99 // NewHistogram implements Provider. Help is ignored.
100 func (p statsdProvider) NewHistogram(name, _ string, min, max int64, sigfigs int, quantiles ...int) (metrics.Histogram, error) {
101 return p.e.NewHistogram(name), nil
102 }
103
104 // NewGauge implements Provider. Help is ignored.
105 func (p statsdProvider) NewGauge(name, _ string) metrics.Gauge {
106 return p.e.NewGauge(name)
107 }
108
109 // Stop will call the underlying statsd.Emitter's Stop method.
110 func (p statsdProvider) Stop() {
111 p.e.Stop()
112 }
113
114 // NewDogStatsdProvider will return a Provider implementation that is a simple
115 // wrapper around a dogstatsd.Emitter. All metric names will be prefixed with
116 // the given value and data will be emitted once every interval or when the
117 // buffer has reached its max size. If no network value is given, it will
118 // default to "udp".
119 func NewDogStatsdProvider(network, address, prefix string, interval time.Duration, logger log.Logger) (Provider, error) {
120 if network == "" {
121 network = "udp"
122 }
123 if address == "" {
124 return nil, errors.New("address is required")
125 }
126 return dogstatsdProvider{
127 e: dogstatsd.NewEmitter(network, address, prefix, interval, logger),
128 }, nil
129 }
130
131 type dogstatsdProvider struct {
132 e *dogstatsd.Emitter
133 }
134
135 var _ Provider = dogstatsdProvider{}
136
137 // NewCounter implements Provider. Help is ignored.
138 func (p dogstatsdProvider) NewCounter(name, _ string) metrics.Counter {
139 return p.e.NewCounter(name)
140 }
141
142 // NewHistogram implements Provider. Help is ignored.
143 func (p dogstatsdProvider) NewHistogram(name, _ string, min, max int64, sigfigs int, quantiles ...int) (metrics.Histogram, error) {
144 return p.e.NewHistogram(name), nil
145 }
146
147 // NewGauge implements Provider. Help is ignored.
148 func (p dogstatsdProvider) NewGauge(name, _ string) metrics.Gauge {
149 return p.e.NewGauge(name)
150 }
151
152 // Stop will call the underlying statsd.Emitter's Stop method.
153 func (p dogstatsdProvider) Stop() {
154 p.e.Stop()
155 }
156
157 // NewExpvarProvider is a very thin wrapper over the expvar package.
158 // If a prefix is provided, it will prefix all metric names.
159 func NewExpvarProvider(prefix string) Provider {
160 return expvarProvider{prefix: prefix}
161 }
162
163 type expvarProvider struct {
164 prefix string
165 }
166
167 var _ Provider = expvarProvider{}
168
169 // NewCounter implements Provider. Help is ignored.
170 func (p expvarProvider) NewCounter(name, _ string) metrics.Counter {
171 return kitexp.NewCounter(p.prefix + name)
172 }
173
174 // NewHistogram implements Provider. Help is ignored.
175 func (p expvarProvider) NewHistogram(name, _ string, min, max int64, sigfigs int, quantiles ...int) (metrics.Histogram, error) {
176 return kitexp.NewHistogram(p.prefix+name, min, max, sigfigs, quantiles...), nil
177 }
178
179 // NewGauge implements Provider. Help is ignored.
180 func (p expvarProvider) NewGauge(name, _ string) metrics.Gauge {
181 return kitexp.NewGauge(p.prefix + name)
182 }
183
184 // Stop is a no-op.
185 func (expvarProvider) Stop() {}
186
187 type prometheusProvider struct {
188 namespace string
189 subsystem string
190 }
191
192 var _ Provider = prometheusProvider{}
193
194 // NewPrometheusProvider returns a Prometheus provider that uses the provided
195 // namespace and subsystem for all metrics.
196 func NewPrometheusProvider(namespace, subsystem string) Provider {
197 return prometheusProvider{
198 namespace: namespace,
199 subsystem: subsystem,
200 }
201 }
202
203 // NewCounter implements Provider.
204 func (p prometheusProvider) NewCounter(name, help string) metrics.Counter {
205 return kitprom.NewCounter(prometheus.CounterOpts{
206 Namespace: p.namespace,
207 Subsystem: p.subsystem,
208 Name: name,
209 Help: help,
210 }, nil)
211 }
212
213 // NewHistogram ignores all parameters except name and help.
214 func (p prometheusProvider) NewHistogram(name, help string, _, _ int64, _ int, _ ...int) (metrics.Histogram, error) {
215 return kitprom.NewHistogram(prometheus.HistogramOpts{
216 Namespace: p.namespace,
217 Subsystem: p.subsystem,
218 Name: name,
219 Help: help,
220 }, nil), nil
221 }
222
223 // NewGauge implements Provider.
224 func (p prometheusProvider) NewGauge(name, help string) metrics.Gauge {
225 return kitprom.NewGauge(prometheus.GaugeOpts{
226 Namespace: p.namespace,
227 Subsystem: p.subsystem,
228 Name: name,
229 Help: help,
230 }, nil)
231 }
232
233 // Stop is a no-op.
234 func (prometheusProvider) Stop() {}
235
236 var _ Provider = discardProvider{}
237
238 // NewDiscardProvider returns a provider that will discard all metrics.
239 func NewDiscardProvider() Provider {
240 return discardProvider{}
241 }
242
243 type discardProvider struct{}
244
245 func (p discardProvider) NewCounter(name string, _ string) metrics.Counter {
246 return discard.NewCounter(name)
247 }
248
249 func (p discardProvider) NewHistogram(name string, _ string, _ int64, _ int64, _ int, _ ...int) (metrics.Histogram, error) {
250 return discard.NewHistogram(name), nil
251 }
252
253 func (p discardProvider) NewGauge(name string, _ string) metrics.Gauge {
254 return discard.NewGauge(name)
255 }
256
257 // Stop is a no-op.
258 func (p discardProvider) Stop() {}
+0
-56
metrics/provider/providers_test.go less more
0 package provider
1
2 import (
3 "testing"
4 "time"
5
6 "github.com/go-kit/kit/log"
7 )
8
9 func TestGraphite(t *testing.T) {
10 p, err := NewGraphiteProvider("network", "address", "prefix", time.Second, log.NewNopLogger())
11 if err != nil {
12 t.Fatal(err)
13 }
14 testProvider(t, "Graphite", p)
15 }
16
17 func TestStatsd(t *testing.T) {
18 p, err := NewStatsdProvider("network", "address", "prefix", time.Second, log.NewNopLogger())
19 if err != nil {
20 t.Fatal(err)
21 }
22 testProvider(t, "Statsd", p)
23 }
24
25 func TestDogStatsd(t *testing.T) {
26 p, err := NewDogStatsdProvider("network", "address", "prefix", time.Second, log.NewNopLogger())
27 if err != nil {
28 t.Fatal(err)
29 }
30 testProvider(t, "DogStatsd", p)
31 }
32
33 func TestExpvar(t *testing.T) {
34 testProvider(t, "Expvar", NewExpvarProvider("prefix"))
35 }
36
37 func TestPrometheus(t *testing.T) {
38 testProvider(t, "Prometheus", NewPrometheusProvider("namespace", "subsystem"))
39 }
40
41 func testProvider(t *testing.T, what string, p Provider) {
42 c := p.NewCounter("counter", "Counter help.")
43 c.Add(1)
44
45 h, err := p.NewHistogram("histogram", "Histogram help.", 1, 100, 3, 50, 95, 99)
46 if err != nil {
47 t.Errorf("%s: NewHistogram: %v", what, err)
48 }
49 h.Observe(99)
50
51 g := p.NewGauge("gauge", "Gauge help.")
52 g.Set(123)
53
54 p.Stop()
55 }
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/statsd"
5 )
6
7 type statsdProvider struct {
8 s *statsd.Statsd
9 stop func()
10 }
11
12 // NewStatsdProvider wraps the given Statsd object and stop func and returns a
13 // Provider that produces Statsd metrics. A typical stop function would be
14 // ticker.Stop from the ticker passed to the SendLoop helper method.
15 func NewStatsdProvider(s *statsd.Statsd, stop func()) Provider {
16 return &statsdProvider{
17 s: s,
18 stop: stop,
19 }
20 }
21
22 // NewCounter implements Provider.
23 func (p *statsdProvider) NewCounter(name string) metrics.Counter {
24 return p.s.NewCounter(name, 1.0)
25 }
26
27 // NewGauge implements Provider.
28 func (p *statsdProvider) NewGauge(name string) metrics.Gauge {
29 return p.s.NewGauge(name)
30 }
31
32 // NewHistogram implements Provider, returning a StatsD Timing that accepts
33 // observations in milliseconds. The sample rate is fixed at 1.0. The bucket
34 // parameter is ignored.
35 func (p *statsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
36 return p.s.NewTiming(name, 1.0)
37 }
38
39 // Stop implements Provider, invoking the stop function passed at construction.
40 func (p *statsdProvider) Stop() {
41 p.stop()
42 }
+0
-23
metrics/scaled_histogram.go less more
0 package metrics
1
2 type scaledHistogram struct {
3 Histogram
4 scale int64
5 }
6
7 // NewScaledHistogram returns a Histogram whose observed values are downscaled
8 // (divided) by scale.
9 func NewScaledHistogram(h Histogram, scale int64) Histogram {
10 return scaledHistogram{h, scale}
11 }
12
13 func (h scaledHistogram) With(f Field) Histogram {
14 return scaledHistogram{
15 Histogram: h.Histogram.With(f),
16 scale: h.scale,
17 }
18 }
19
20 func (h scaledHistogram) Observe(value int64) {
21 h.Histogram.Observe(value / h.scale)
22 }
+0
-26
metrics/scaled_histogram_test.go less more
0 package metrics_test
1
2 import (
3 "testing"
4
5 "github.com/go-kit/kit/metrics"
6 "github.com/go-kit/kit/metrics/expvar"
7 "github.com/go-kit/kit/metrics/teststat"
8 )
9
10 func TestScaledHistogram(t *testing.T) {
11 var (
12 quantiles = []int{50, 90, 99}
13 scale = int64(10)
14 metricName = "test_scaled_histogram"
15 )
16
17 var h metrics.Histogram
18 h = expvar.NewHistogram(metricName, 0, 1000, 3, quantiles...)
19 h = metrics.NewScaledHistogram(h, scale)
20 h = h.With(metrics.Field{Key: "a", Value: "b"})
21
22 const seed, mean, stdev = 333, 500, 100 // input values
23 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) // will be scaled down
24 assertExpvarNormalHistogram(t, metricName, mean/scale, stdev/scale, quantiles)
25 }
+0
-159
metrics/statsd/emitter.go less more
0 package statsd
1
2 import (
3 "bytes"
4 "fmt"
5 "net"
6 "time"
7
8 "github.com/go-kit/kit/log"
9 "github.com/go-kit/kit/metrics"
10 "github.com/go-kit/kit/util/conn"
11 )
12
13 // Emitter is a struct to manage connections and orchestrate the emission of
14 // metrics to a Statsd process.
15 type Emitter struct {
16 prefix string
17 keyVals chan keyVal
18 mgr *conn.Manager
19 logger log.Logger
20 quitc chan chan struct{}
21 }
22
23 type keyVal struct {
24 key string
25 val string
26 }
27
28 func stringToKeyVal(key string, keyVals chan keyVal) chan string {
29 vals := make(chan string)
30 go func() {
31 for val := range vals {
32 keyVals <- keyVal{key: key, val: val}
33 }
34 }()
35 return vals
36 }
37
38 // NewEmitter will return an Emitter that will prefix all metrics names with the
39 // given prefix. Once started, it will attempt to create a connection with the
40 // given network and address via `net.Dial` and periodically post metrics to the
41 // connection in the statsd protocol.
42 func NewEmitter(network, address string, metricsPrefix string, flushInterval time.Duration, logger log.Logger) *Emitter {
43 return NewEmitterDial(net.Dial, network, address, metricsPrefix, flushInterval, logger)
44 }
45
46 // NewEmitterDial is the same as NewEmitter, but allows you to specify your own
47 // Dialer function. This is primarily useful for tests.
48 func NewEmitterDial(dialer conn.Dialer, network, address string, metricsPrefix string, flushInterval time.Duration, logger log.Logger) *Emitter {
49 e := &Emitter{
50 prefix: metricsPrefix,
51 mgr: conn.NewManager(dialer, network, address, time.After, logger),
52 logger: logger,
53 keyVals: make(chan keyVal),
54 quitc: make(chan chan struct{}),
55 }
56 go e.loop(flushInterval)
57 return e
58 }
59
60 // NewCounter returns a Counter that emits observations in the statsd protocol
61 // via the Emitter's connection manager. Observations are buffered for the
62 // report interval or until the buffer exceeds a max packet size, whichever
63 // comes first. Fields are ignored.
64 func (e *Emitter) NewCounter(key string) metrics.Counter {
65 key = e.prefix + key
66 return &counter{
67 key: key,
68 c: stringToKeyVal(key, e.keyVals),
69 }
70 }
71
72 // NewHistogram returns a Histogram that emits observations in the statsd
73 // protocol via the Emitter's connection manager. Observations are buffered for
74 // the reporting interval or until the buffer exceeds a max packet size,
75 // whichever comes first. Fields are ignored.
76 //
77 // NewHistogram is mapped to a statsd Timing, so observations should represent
78 // milliseconds. If you observe in units of nanoseconds, you can make the
79 // translation with a ScaledHistogram:
80 //
81 // NewScaledHistogram(histogram, time.Millisecond)
82 //
83 // You can also enforce the constraint in a typesafe way with a millisecond
84 // TimeHistogram:
85 //
86 // NewTimeHistogram(histogram, time.Millisecond)
87 //
88 // TODO: support for sampling.
89 func (e *Emitter) NewHistogram(key string) metrics.Histogram {
90 key = e.prefix + key
91 return &histogram{
92 key: key,
93 h: stringToKeyVal(key, e.keyVals),
94 }
95 }
96
97 // NewGauge returns a Gauge that emits values in the statsd protocol via the
98 // the Emitter's connection manager. Values are buffered for the report
99 // interval or until the buffer exceeds a max packet size, whichever comes
100 // first. Fields are ignored.
101 //
102 // TODO: support for sampling
103 func (e *Emitter) NewGauge(key string) metrics.Gauge {
104 key = e.prefix + key
105 return &gauge{
106 key: key,
107 g: stringToKeyVal(key, e.keyVals),
108 }
109 }
110
111 func (e *Emitter) loop(d time.Duration) {
112 ticker := time.NewTicker(d)
113 defer ticker.Stop()
114 buf := &bytes.Buffer{}
115 for {
116 select {
117 case kv := <-e.keyVals:
118 fmt.Fprintf(buf, "%s:%s\n", kv.key, kv.val)
119 if buf.Len() > maxBufferSize {
120 e.Flush(buf)
121 }
122
123 case <-ticker.C:
124 e.Flush(buf)
125
126 case q := <-e.quitc:
127 e.Flush(buf)
128 close(q)
129 return
130 }
131 }
132 }
133
134 // Stop will flush the current metrics and close the active connection. Calling
135 // stop more than once is a programmer error.
136 func (e *Emitter) Stop() {
137 q := make(chan struct{})
138 e.quitc <- q
139 <-q
140 }
141
142 // Flush will write the given buffer to a connection provided by the Emitter's
143 // connection manager.
144 func (e *Emitter) Flush(buf *bytes.Buffer) {
145 conn := e.mgr.Take()
146 if conn == nil {
147 e.logger.Log("during", "flush", "err", "connection unavailable")
148 return
149 }
150
151 _, err := conn.Write(buf.Bytes())
152 if err != nil {
153 e.logger.Log("during", "flush", "err", err)
154 }
155 buf.Reset()
156
157 e.mgr.Put(err)
158 }
0 // Package statsd implements a statsd backend for package metrics.
0 // Package statsd provides a StatsD backend for package metrics. StatsD has no
1 // concept of arbitrary key-value tagging, so label values are not supported,
2 // and With is a no-op on all metrics.
13 //
2 // The current implementation ignores fields. In the future, it would be good
3 // to have an implementation that accepted a set of predeclared field names at
4 // construction time, and used field values to produce delimiter-separated
5 // bucket (key) names. That is,
6 //
7 // c := NewFieldedCounter(..., "path", "status")
8 // c.Add(1) // "myprefix.unknown.unknown:1|c\n"
9 // c2 := c.With("path", "foo").With("status": "200")
10 // c2.Add(1) // "myprefix.foo.200:1|c\n"
11 //
4 // This package batches observations and emits them on some schedule to the
5 // remote server. This is useful even if you connect to your StatsD server over
6 // UDP. Emitting one network packet per observation can quickly overwhelm even
7 // the fastest internal network.
128 package statsd
139
1410 import (
15 "bytes"
1611 "fmt"
1712 "io"
18 "log"
19 "math"
2013 "time"
2114
22 "sync/atomic"
23
24 "github.com/go-kit/kit/metrics"
15 "github.com/go-kit/kit/log"
16 "github.com/go-kit/kit/metrics3"
17 "github.com/go-kit/kit/metrics3/internal/lv"
18 "github.com/go-kit/kit/metrics3/internal/ratemap"
19 "github.com/go-kit/kit/util/conn"
2520 )
2621
27 // statsd metrics take considerable influence from
28 // https://github.com/streadway/handy package statsd.
29
30 const maxBufferSize = 1400 // bytes
31
32 type counter struct {
33 key string
34 c chan string
35 }
36
37 // NewCounter returns a Counter that emits observations in the statsd protocol
38 // to the passed writer. Observations are buffered for the report interval or
39 // until the buffer exceeds a max packet size, whichever comes first. Fields
40 // are ignored.
22 // Statsd receives metrics observations and forwards them to a StatsD server.
23 // Create a Statsd object, use it to create metrics, and pass those metrics as
24 // dependencies to the components that will use them.
4125 //
42 // TODO: support for sampling.
43 func NewCounter(w io.Writer, key string, reportInterval time.Duration) metrics.Counter {
44 return NewCounterTick(w, key, time.Tick(reportInterval))
45 }
46
47 // NewCounterTick is the same as NewCounter, but allows the user to pass in a
48 // ticker channel instead of invoking time.Tick.
49 func NewCounterTick(w io.Writer, key string, reportTicker <-chan time.Time) metrics.Counter {
50 c := &counter{
51 key: key,
52 c: make(chan string),
53 }
54 go fwd(w, key, reportTicker, c.c)
26 // All metrics are buffered until WriteTo is called. Counters and gauges are
27 // aggregated into a single observation per timeseries per write. Timings are
28 // buffered but not aggregated.
29 //
30 // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
31 // To send to a StatsD server, use the SendLoop helper method.
32 type Statsd struct {
33 prefix string
34 rates *ratemap.RateMap
35
36 // The observations are collected in an N-dimensional vector space, even
37 // though they only take advantage of a single dimension (name). This is an
38 // implementation detail born purely from convenience. It would be more
39 // accurate to collect them in a map[string][]float64, but we already have
40 // this nice data structure and helper methods.
41 counters *lv.Space
42 gauges *lv.Space
43 timings *lv.Space
44
45 logger log.Logger
46 }
47
48 // New returns a Statsd object that may be used to create metrics. Prefix is
49 // applied to all created metrics. Callers must ensure that regular calls to
50 // WriteTo are performed, either manually or with one of the helper methods.
51 func New(prefix string, logger log.Logger) *Statsd {
52 return &Statsd{
53 prefix: prefix,
54 rates: ratemap.New(),
55 counters: lv.NewSpace(),
56 gauges: lv.NewSpace(),
57 timings: lv.NewSpace(),
58 logger: logger,
59 }
60 }
61
62 // NewCounter returns a counter, sending observations to this Statsd object.
63 func (s *Statsd) NewCounter(name string, sampleRate float64) *Counter {
64 s.rates.Set(s.prefix+name, sampleRate)
65 return &Counter{
66 name: s.prefix + name,
67 obs: s.counters.Observe,
68 }
69 }
70
71 // NewGauge returns a gauge, sending observations to this Statsd object.
72 func (s *Statsd) NewGauge(name string) *Gauge {
73 return &Gauge{
74 name: s.prefix + name,
75 obs: s.gauges.Observe,
76 }
77 }
78
79 // NewTiming returns a histogram whose observations are interpreted as
80 // millisecond durations, and are forwarded to this Statsd object.
81 func (s *Statsd) NewTiming(name string, sampleRate float64) *Timing {
82 s.rates.Set(s.prefix+name, sampleRate)
83 return &Timing{
84 name: s.prefix + name,
85 obs: s.timings.Observe,
86 }
87 }
88
89 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
90 // time the passed channel fires. This method blocks until the channel is
91 // closed, so clients probably want to run it in its own goroutine. For typical
92 // usage, create a time.Ticker and pass its C channel to this method.
93 func (s *Statsd) WriteLoop(c <-chan time.Time, w io.Writer) {
94 for range c {
95 if _, err := s.WriteTo(w); err != nil {
96 s.logger.Log("during", "WriteTo", "err", err)
97 }
98 }
99 }
100
101 // SendLoop is a helper method that wraps WriteLoop, passing a managed
102 // connection to the network and address. Like WriteLoop, this method blocks
103 // until the channel is closed, so clients probably want to start it in its own
104 // goroutine. For typical usage, create a time.Ticker and pass its C channel to
105 // this method.
106 func (s *Statsd) SendLoop(c <-chan time.Time, network, address string) {
107 s.WriteLoop(c, conn.NewDefaultManager(network, address, s.logger))
108 }
109
110 // WriteTo flushes the buffered content of the metrics to the writer, in
111 // StatsD format. WriteTo abides best-effort semantics, so observations are
112 // lost if there is a problem with the write. Clients should be sure to call
113 // WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
114 func (s *Statsd) WriteTo(w io.Writer) (count int64, err error) {
115 var n int
116
117 s.counters.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
118 n, err = fmt.Fprintf(w, "%s:%f|c%s\n", name, sum(values), sampling(s.rates.Get(name)))
119 if err != nil {
120 return false
121 }
122 count += int64(n)
123 return true
124 })
125 if err != nil {
126 return count, err
127 }
128
129 s.gauges.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
130 n, err = fmt.Fprintf(w, "%s:%f|g\n", name, last(values))
131 if err != nil {
132 return false
133 }
134 count += int64(n)
135 return true
136 })
137 if err != nil {
138 return count, err
139 }
140
141 s.timings.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
142 sampleRate := s.rates.Get(name)
143 for _, value := range values {
144 n, err = fmt.Fprintf(w, "%s:%f|ms%s\n", name, value, sampling(sampleRate))
145 if err != nil {
146 return false
147 }
148 count += int64(n)
149 }
150 return true
151 })
152 if err != nil {
153 return count, err
154 }
155
156 return count, err
157 }
158
159 func sum(a []float64) float64 {
160 var v float64
161 for _, f := range a {
162 v += f
163 }
164 return v
165 }
166
167 func last(a []float64) float64 {
168 return a[len(a)-1]
169 }
170
171 func sampling(r float64) string {
172 var sv string
173 if r < 1.0 {
174 sv = fmt.Sprintf("|@%f", r)
175 }
176 return sv
177 }
178
179 type observeFunc func(name string, lvs lv.LabelValues, value float64)
180
181 // Counter is a StatsD counter. Observations are forwarded to a Statsd object,
182 // and aggregated (summed) per timeseries.
183 type Counter struct {
184 name string
185 obs observeFunc
186 }
187
188 // With is a no-op.
189 func (c *Counter) With(...string) metrics.Counter {
55190 return c
56191 }
57192
58 func (c *counter) Name() string { return c.key }
59
60 func (c *counter) With(metrics.Field) metrics.Counter { return c }
61
62 func (c *counter) Add(delta uint64) { c.c <- fmt.Sprintf("%d|c", delta) }
63
64 type gauge struct {
65 key string
66 lastValue uint64 // math.Float64frombits
67 g chan string
68 }
69
70 // NewGauge returns a Gauge that emits values in the statsd protocol to the
71 // passed writer. Values are buffered for the report interval or until the
72 // buffer exceeds a max packet size, whichever comes first. Fields are
73 // ignored.
74 //
75 // TODO: support for sampling.
76 func NewGauge(w io.Writer, key string, reportInterval time.Duration) metrics.Gauge {
77 return NewGaugeTick(w, key, time.Tick(reportInterval))
78 }
79
80 // NewGaugeTick is the same as NewGauge, but allows the user to pass in a ticker
81 // channel instead of invoking time.Tick.
82 func NewGaugeTick(w io.Writer, key string, reportTicker <-chan time.Time) metrics.Gauge {
83 g := &gauge{
84 key: key,
85 g: make(chan string),
86 }
87 go fwd(w, key, reportTicker, g.g)
193 // Add implements metrics.Counter.
194 func (c *Counter) Add(delta float64) {
195 c.obs(c.name, lv.LabelValues{}, delta)
196 }
197
198 // Gauge is a StatsD gauge. Observations are forwarded to a Statsd object, and
199 // aggregated (the last observation selected) per timeseries.
200 type Gauge struct {
201 name string
202 obs observeFunc
203 }
204
205 // With is a no-op.
206 func (g *Gauge) With(...string) metrics.Gauge {
88207 return g
89208 }
90209
91 func (g *gauge) Name() string { return g.key }
92
93 func (g *gauge) With(metrics.Field) metrics.Gauge { return g }
94
95 func (g *gauge) Add(delta float64) {
96 // https://github.com/etsy/statsd/blob/master/docs/metric_types.md#gauges
97 sign := "+"
98 if delta < 0 {
99 sign, delta = "-", -delta
100 }
101 g.g <- fmt.Sprintf("%s%f|g", sign, delta)
102 }
103
104 func (g *gauge) Set(value float64) {
105 atomic.StoreUint64(&g.lastValue, math.Float64bits(value))
106 g.g <- fmt.Sprintf("%f|g", value)
107 }
108
109 func (g *gauge) Get() float64 {
110 return math.Float64frombits(atomic.LoadUint64(&g.lastValue))
111 }
112
113 // NewCallbackGauge emits values in the statsd protocol to the passed writer.
114 // It collects values every scrape interval from the callback. Values are
115 // buffered for the report interval or until the buffer exceeds a max packet
116 // size, whichever comes first. The report and scrape intervals may be the
117 // same. The callback determines the value, and fields are ignored, so
118 // NewCallbackGauge returns nothing.
119 func NewCallbackGauge(w io.Writer, key string, reportInterval, scrapeInterval time.Duration, callback func() float64) {
120 NewCallbackGaugeTick(w, key, time.Tick(reportInterval), time.Tick(scrapeInterval), callback)
121 }
122
123 // NewCallbackGaugeTick is the same as NewCallbackGauge, but allows the user to
124 // pass in ticker channels instead of durations to control report and scrape
125 // intervals.
126 func NewCallbackGaugeTick(w io.Writer, key string, reportTicker, scrapeTicker <-chan time.Time, callback func() float64) {
127 go fwd(w, key, reportTicker, emitEvery(scrapeTicker, callback))
128 }
129
130 func emitEvery(emitTicker <-chan time.Time, callback func() float64) <-chan string {
131 c := make(chan string)
132 go func() {
133 for range emitTicker {
134 c <- fmt.Sprintf("%f|g", callback())
135 }
136 }()
137 return c
138 }
139
140 type histogram struct {
141 key string
142 h chan string
143 }
144
145 // NewHistogram returns a Histogram that emits observations in the statsd
146 // protocol to the passed writer. Observations are buffered for the reporting
147 // interval or until the buffer exceeds a max packet size, whichever comes
148 // first. Fields are ignored.
149 //
150 // NewHistogram is mapped to a statsd Timing, so observations should represent
151 // milliseconds. If you observe in units of nanoseconds, you can make the
152 // translation with a ScaledHistogram:
153 //
154 // NewScaledHistogram(statsdHistogram, time.Millisecond)
155 //
156 // You can also enforce the constraint in a typesafe way with a millisecond
157 // TimeHistogram:
158 //
159 // NewTimeHistogram(statsdHistogram, time.Millisecond)
160 //
161 // TODO: support for sampling.
162 func NewHistogram(w io.Writer, key string, reportInterval time.Duration) metrics.Histogram {
163 return NewHistogramTick(w, key, time.Tick(reportInterval))
164 }
165
166 // NewHistogramTick is the same as NewHistogram, but allows the user to pass a
167 // ticker channel instead of invoking time.Tick.
168 func NewHistogramTick(w io.Writer, key string, reportTicker <-chan time.Time) metrics.Histogram {
169 h := &histogram{
170 key: key,
171 h: make(chan string),
172 }
173 go fwd(w, key, reportTicker, h.h)
174 return h
175 }
176
177 func (h *histogram) Name() string { return h.key }
178
179 func (h *histogram) With(metrics.Field) metrics.Histogram { return h }
180
181 func (h *histogram) Observe(value int64) {
182 h.h <- fmt.Sprintf("%d|ms", value)
183 }
184
185 func (h *histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
186 // TODO(pb): no way to do this without introducing e.g. codahale/hdrhistogram
187 return []metrics.Bucket{}, []metrics.Quantile{}
188 }
189
190 func fwd(w io.Writer, key string, reportTicker <-chan time.Time, c <-chan string) {
191 buf := &bytes.Buffer{}
192 for {
193 select {
194 case s := <-c:
195 fmt.Fprintf(buf, "%s:%s\n", key, s)
196 if buf.Len() > maxBufferSize {
197 flush(w, buf)
198 }
199
200 case <-reportTicker:
201 flush(w, buf)
202 }
203 }
204 }
205
206 func flush(w io.Writer, buf *bytes.Buffer) {
207 if buf.Len() <= 0 {
208 return
209 }
210 if _, err := w.Write(buf.Bytes()); err != nil {
211 log.Printf("error: could not write to statsd: %v", err)
212 }
213 buf.Reset()
214 }
210 // Set implements metrics.Gauge.
211 func (g *Gauge) Set(value float64) {
212 g.obs(g.name, lv.LabelValues{}, value)
213 }
214
215 // Timing is a StatsD timing, or metrics.Histogram. Observations are
216 // forwarded to a Statsd object, and collected (but not aggregated) per
217 // timeseries.
218 type Timing struct {
219 name string
220 obs observeFunc
221 }
222
223 // With is a no-op.
224 func (t *Timing) With(...string) metrics.Histogram {
225 return t
226 }
227
228 // Observe implements metrics.Histogram. Value is interpreted as milliseconds.
229 func (t *Timing) Observe(value float64) {
230 t.obs(t.name, lv.LabelValues{}, value)
231 }
00 package statsd
11
22 import (
3 "bytes"
4 "fmt"
5 "net"
6 "strings"
7 "sync"
83 "testing"
9 "time"
104
115 "github.com/go-kit/kit/log"
12 "github.com/go-kit/kit/util/conn"
6 "github.com/go-kit/kit/metrics3/teststat"
137 )
148
15 func TestEmitterCounter(t *testing.T) {
16 e, buf := testEmitter()
17
18 c := e.NewCounter("test_statsd_counter")
19 c.Add(1)
20 c.Add(2)
21
22 // give time for things to emit
23 time.Sleep(time.Millisecond * 250)
24 // force a flush and stop
25 e.Stop()
26
27 want := "prefix.test_statsd_counter:1|c\nprefix.test_statsd_counter:2|c\n"
28 have := buf.String()
29 if want != have {
30 t.Errorf("want %q, have %q", want, have)
9 func TestCounter(t *testing.T) {
10 prefix, name := "abc.", "def"
11 label, value := "label", "value" // ignored
12 regex := `^` + prefix + name + `:([0-9\.]+)\|c$`
13 s := New(prefix, log.NewNopLogger())
14 counter := s.NewCounter(name, 1.0).With(label, value)
15 valuef := teststat.SumLines(s, regex)
16 if err := teststat.TestCounter(counter, valuef); err != nil {
17 t.Fatal(err)
3118 }
3219 }
3320
34 func TestEmitterGauge(t *testing.T) {
35 e, buf := testEmitter()
21 func TestCounterSampled(t *testing.T) {
22 // This will involve multiplying the observed sum by the inverse of the
23 // sample rate and checking against the expected value within some
24 // tolerance.
25 t.Skip("TODO")
26 }
3627
37 g := e.NewGauge("test_statsd_gauge")
38
39 delta := 1.0
40 g.Add(delta)
41
42 // give time for things to emit
43 time.Sleep(time.Millisecond * 250)
44 // force a flush and stop
45 e.Stop()
46
47 want := fmt.Sprintf("prefix.test_statsd_gauge:+%f|g\n", delta)
48 have := buf.String()
49 if want != have {
50 t.Errorf("want %q, have %q", want, have)
28 func TestGauge(t *testing.T) {
29 prefix, name := "ghi.", "jkl"
30 label, value := "xyz", "abc" // ignored
31 regex := `^` + prefix + name + `:([0-9\.]+)\|g$`
32 s := New(prefix, log.NewNopLogger())
33 gauge := s.NewGauge(name).With(label, value)
34 valuef := teststat.LastLine(s, regex)
35 if err := teststat.TestGauge(gauge, valuef); err != nil {
36 t.Fatal(err)
5137 }
5238 }
5339
54 func TestEmitterHistogram(t *testing.T) {
55 e, buf := testEmitter()
56 h := e.NewHistogram("test_statsd_histogram")
40 // StatsD timings just emit all observations. So, we collect them into a generic
41 // histogram, and run the statistics test on that.
5742
58 h.Observe(123)
59
60 // give time for things to emit
61 time.Sleep(time.Millisecond * 250)
62 // force a flush and stop
63 e.Stop()
64
65 want := "prefix.test_statsd_histogram:123|ms\n"
66 have := buf.String()
67 if want != have {
68 t.Errorf("want %q, have %q", want, have)
43 func TestTiming(t *testing.T) {
44 prefix, name := "statsd.", "timing_test"
45 label, value := "abc", "def" // ignored
46 regex := `^` + prefix + name + `:([0-9\.]+)\|ms$`
47 s := New(prefix, log.NewNopLogger())
48 timing := s.NewTiming(name, 1.0).With(label, value)
49 quantiles := teststat.Quantiles(s, regex, 50) // no |@0.X
50 if err := teststat.TestHistogram(timing, quantiles, 0.01); err != nil {
51 t.Fatal(err)
6952 }
7053 }
7154
72 func TestCounter(t *testing.T) {
73 buf := &syncbuf{buf: &bytes.Buffer{}}
74 reportc := make(chan time.Time)
75 c := NewCounterTick(buf, "test_statsd_counter", reportc)
76
77 c.Add(1)
78 c.Add(2)
79
80 want, have := "test_statsd_counter:1|c\ntest_statsd_counter:2|c\n", ""
81 by(t, 100*time.Millisecond, func() bool {
82 have = buf.String()
83 return want == have
84 }, func() {
85 reportc <- time.Now()
86 }, fmt.Sprintf("want %q, have %q", want, have))
87 }
88
89 func TestGauge(t *testing.T) {
90 buf := &syncbuf{buf: &bytes.Buffer{}}
91 reportc := make(chan time.Time)
92 g := NewGaugeTick(buf, "test_statsd_gauge", reportc)
93
94 delta := 1.0
95 g.Add(delta)
96
97 want, have := fmt.Sprintf("test_statsd_gauge:+%f|g\n", delta), ""
98 by(t, 100*time.Millisecond, func() bool {
99 have = buf.String()
100 return want == have
101 }, func() {
102 reportc <- time.Now()
103 }, fmt.Sprintf("want %q, have %q", want, have))
104
105 buf.Reset()
106 delta = -2.0
107 g.Add(delta)
108
109 want, have = fmt.Sprintf("test_statsd_gauge:%f|g\n", delta), ""
110 by(t, 100*time.Millisecond, func() bool {
111 have = buf.String()
112 return want == have
113 }, func() {
114 reportc <- time.Now()
115 }, fmt.Sprintf("want %q, have %q", want, have))
116
117 buf.Reset()
118 value := 3.0
119 g.Set(value)
120
121 want, have = fmt.Sprintf("test_statsd_gauge:%f|g\n", value), ""
122 by(t, 100*time.Millisecond, func() bool {
123 have = buf.String()
124 return want == have
125 }, func() {
126 reportc <- time.Now()
127 }, fmt.Sprintf("want %q, have %q", want, have))
128 }
129
130 func TestCallbackGauge(t *testing.T) {
131 buf := &syncbuf{buf: &bytes.Buffer{}}
132 reportc, scrapec := make(chan time.Time), make(chan time.Time)
133 value := 55.55
134 cb := func() float64 { return value }
135 NewCallbackGaugeTick(buf, "test_statsd_callback_gauge", reportc, scrapec, cb)
136
137 scrapec <- time.Now()
138 reportc <- time.Now()
139
140 // Travis is annoying
141 by(t, time.Second, func() bool {
142 return buf.String() != ""
143 }, func() {
144 reportc <- time.Now()
145 }, "buffer never got write+flush")
146
147 want, have := fmt.Sprintf("test_statsd_callback_gauge:%f|g\n", value), ""
148 by(t, 100*time.Millisecond, func() bool {
149 have = buf.String()
150 return strings.HasPrefix(have, want) // HasPrefix because we might get multiple writes
151 }, func() {
152 reportc <- time.Now()
153 }, fmt.Sprintf("want %q, have %q", want, have))
154 }
155
156 func TestHistogram(t *testing.T) {
157 buf := &syncbuf{buf: &bytes.Buffer{}}
158 reportc := make(chan time.Time)
159 h := NewHistogramTick(buf, "test_statsd_histogram", reportc)
160
161 h.Observe(123)
162
163 want, have := "test_statsd_histogram:123|ms\n", ""
164 by(t, 100*time.Millisecond, func() bool {
165 have = buf.String()
166 return want == have
167 }, func() {
168 reportc <- time.Now()
169 }, fmt.Sprintf("want %q, have %q", want, have))
170 }
171
172 func by(t *testing.T, d time.Duration, check func() bool, execute func(), msg string) {
173 deadline := time.Now().Add(d)
174 for !check() {
175 if time.Now().After(deadline) {
176 t.Fatal(msg)
177 }
178 execute()
55 func TestTimingSampled(t *testing.T) {
56 prefix, name := "statsd.", "sampled_timing_test"
57 label, value := "foo", "bar" // ignored
58 regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0\.01[0]*$`
59 s := New(prefix, log.NewNopLogger())
60 timing := s.NewTiming(name, 0.01).With(label, value)
61 quantiles := teststat.Quantiles(s, regex, 50)
62 if err := teststat.TestHistogram(timing, quantiles, 0.02); err != nil {
63 t.Fatal(err)
17964 }
18065 }
181
182 type syncbuf struct {
183 mtx sync.Mutex
184 buf *bytes.Buffer
185 }
186
187 func (s *syncbuf) Write(p []byte) (int, error) {
188 s.mtx.Lock()
189 defer s.mtx.Unlock()
190 return s.buf.Write(p)
191 }
192
193 func (s *syncbuf) String() string {
194 s.mtx.Lock()
195 defer s.mtx.Unlock()
196 return s.buf.String()
197 }
198
199 func (s *syncbuf) Reset() {
200 s.mtx.Lock()
201 defer s.mtx.Unlock()
202 s.buf.Reset()
203 }
204
205 func testEmitter() (*Emitter, *syncbuf) {
206 buf := &syncbuf{buf: &bytes.Buffer{}}
207 e := &Emitter{
208 prefix: "prefix.",
209 mgr: conn.NewManager(mockDialer(buf), "", "", time.After, log.NewNopLogger()),
210 logger: log.NewNopLogger(),
211 keyVals: make(chan keyVal),
212 quitc: make(chan chan struct{}),
213 }
214 go e.loop(time.Millisecond * 20)
215 return e, buf
216 }
217
218 func mockDialer(buf *syncbuf) conn.Dialer {
219 return func(net, addr string) (net.Conn, error) {
220 return &mockConn{buf}, nil
221 }
222 }
223
224 type mockConn struct {
225 buf *syncbuf
226 }
227
228 func (c *mockConn) Read(b []byte) (n int, err error) {
229 panic("not implemented")
230 }
231
232 func (c *mockConn) Write(b []byte) (n int, err error) {
233 return c.buf.Write(b)
234 }
235
236 func (c *mockConn) Close() error {
237 panic("not implemented")
238 }
239
240 func (c *mockConn) LocalAddr() net.Addr {
241 panic("not implemented")
242 }
243
244 func (c *mockConn) RemoteAddr() net.Addr {
245 panic("not implemented")
246 }
247
248 func (c *mockConn) SetDeadline(t time.Time) error {
249 panic("not implemented")
250 }
251
252 func (c *mockConn) SetReadDeadline(t time.Time) error {
253 panic("not implemented")
254 }
255
256 func (c *mockConn) SetWriteDeadline(t time.Time) error {
257 panic("not implemented")
258 }
0 package teststat
1
2 import (
3 "bufio"
4 "bytes"
5 "io"
6 "regexp"
7 "strconv"
8
9 "github.com/go-kit/kit/metrics3/generic"
10 )
11
12 // SumLines expects a regex whose first capture group can be parsed as a
13 // float64. It will dump the WriterTo and parse each line, expecting to find a
14 // match. It returns the sum of all captured floats.
15 func SumLines(w io.WriterTo, regex string) func() float64 {
16 return func() float64 {
17 sum, _ := stats(w, regex, nil)
18 return sum
19 }
20 }
21
22 // LastLine expects a regex whose first capture group can be parsed as a
23 // float64. It will dump the WriterTo and parse each line, expecting to find a
24 // match. It returns the final captured float.
25 func LastLine(w io.WriterTo, regex string) func() float64 {
26 return func() float64 {
27 _, final := stats(w, regex, nil)
28 return final
29 }
30 }
31
32 // Quantiles expects a regex whose first capture group can be parsed as a
33 // float64. It will dump the WriterTo and parse each line, expecting to find a
34 // match. It observes all captured floats into a generic.Histogram with the
35 // given number of buckets, and returns the 50th, 90th, 95th, and 99th quantiles
36 // from that histogram.
37 func Quantiles(w io.WriterTo, regex string, buckets int) func() (float64, float64, float64, float64) {
38 return func() (float64, float64, float64, float64) {
39 h := generic.NewHistogram("quantile-test", buckets)
40 stats(w, regex, h)
41 return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99)
42 }
43 }
44
45 func stats(w io.WriterTo, regex string, h *generic.Histogram) (sum, final float64) {
46 re := regexp.MustCompile(regex)
47 buf := &bytes.Buffer{}
48 w.WriteTo(buf)
49 //fmt.Fprintf(os.Stderr, "%s\n", buf.String())
50 s := bufio.NewScanner(buf)
51 for s.Scan() {
52 match := re.FindStringSubmatch(s.Text())
53 f, err := strconv.ParseFloat(match[1], 64)
54 if err != nil {
55 panic(err)
56 }
57 sum += f
58 final = f
59 if h != nil {
60 h.Observe(f)
61 }
62 }
63 return sum, final
64 }
+0
-55
metrics/teststat/circonus.go less more
0 package teststat
1
2 import (
3 "math"
4 "strconv"
5 "strings"
6 "testing"
7
8 "github.com/codahale/hdrhistogram"
9 )
10
11 // AssertCirconusNormalHistogram ensures the Circonus Histogram data captured in
12 // the result slice abides a normal distribution.
13 func AssertCirconusNormalHistogram(t *testing.T, mean, stdev, min, max int64, result []string) {
14 if len(result) <= 0 {
15 t.Fatal("no results")
16 }
17
18 // Circonus just dumps the raw counts. We need to do our own statistical analysis.
19 h := hdrhistogram.New(min, max, 3)
20
21 for _, s := range result {
22 // "H[1.23e04]=123"
23 toks := strings.Split(s, "=")
24 if len(toks) != 2 {
25 t.Fatalf("bad H value: %q", s)
26 }
27
28 var bucket string
29 bucket = toks[0]
30 bucket = bucket[2 : len(bucket)-1] // "H[1.23e04]" -> "1.23e04"
31 f, err := strconv.ParseFloat(bucket, 64)
32 if err != nil {
33 t.Fatalf("error parsing H value: %q: %v", s, err)
34 }
35
36 count, err := strconv.ParseFloat(toks[1], 64)
37 if err != nil {
38 t.Fatalf("error parsing H count: %q: %v", s, err)
39 }
40
41 h.RecordValues(int64(f), int64(count))
42 }
43
44 // Apparently Circonus buckets observations by dropping a sigfig, so we have
45 // very coarse tolerance.
46 var tolerance int64 = 30
47 for _, quantile := range []int{50, 90, 99} {
48 want := normalValueAtQuantile(mean, stdev, quantile)
49 have := h.ValueAtQuantile(float64(quantile))
50 if int64(math.Abs(float64(want)-float64(have))) > tolerance {
51 t.Errorf("quantile %d: want %d, have %d", quantile, want, have)
52 }
53 }
54 }
+0
-73
metrics/teststat/common.go less more
0 // Package teststat contains helper functions for statistical testing of
1 // metrics implementations.
2 package teststat
3
4 import (
5 "math"
6 "math/rand"
7 "testing"
8
9 "github.com/go-kit/kit/metrics"
10 )
11
12 const population = 1234
13
14 // PopulateNormalHistogram populates the Histogram with a normal distribution
15 // of observations.
16 func PopulateNormalHistogram(t *testing.T, h metrics.Histogram, seed int64, mean, stdev int64) {
17 r := rand.New(rand.NewSource(seed))
18 for i := 0; i < population; i++ {
19 sample := int64(r.NormFloat64()*float64(stdev) + float64(mean))
20 if sample < 0 {
21 sample = 0
22 }
23 h.Observe(sample)
24 }
25 }
26
27 // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
28 func normalValueAtQuantile(mean, stdev int64, quantile int) int64 {
29 return int64(float64(mean) + float64(stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1))
30 }
31
32 // https://code.google.com/p/gostat/source/browse/stat/normal.go
33 func observationsLessThan(mean, stdev int64, x float64, total int) int {
34 cdf := ((1.0 / 2.0) * (1 + math.Erf((x-float64(mean))/(float64(stdev)*math.Sqrt2))))
35 return int(cdf * float64(total))
36 }
37
38 // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
39 func erfinv(y float64) float64 {
40 if y < -1.0 || y > 1.0 {
41 panic("invalid input")
42 }
43
44 var (
45 a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
46 b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
47 c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
48 d = [2]float64{3.543889200, 1.637067800}
49 )
50
51 const y0 = 0.7
52 var x, z float64
53
54 if math.Abs(y) == 1.0 {
55 x = -y * math.Log(0.0)
56 } else if y < -y0 {
57 z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
58 x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
59 } else {
60 if y < y0 {
61 z = y * y
62 x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0)
63 } else {
64 z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
65 x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
66 }
67 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
68 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
69 }
70
71 return x
72 }
+0
-26
metrics/teststat/expvar.go less more
0 package teststat
1
2 import (
3 "expvar"
4 "fmt"
5 "math"
6 "strconv"
7 "testing"
8 )
9
10 // AssertExpvarNormalHistogram ensures the expvar Histogram referenced by
11 // metricName abides a normal distribution.
12 func AssertExpvarNormalHistogram(t *testing.T, metricName string, mean, stdev int64, quantiles []int) {
13 const tolerance int = 2
14 for _, quantile := range quantiles {
15 want := normalValueAtQuantile(mean, stdev, quantile)
16 s := expvar.Get(fmt.Sprintf("%s_p%02d", metricName, quantile)).String()
17 have, err := strconv.Atoi(s)
18 if err != nil {
19 t.Fatal(err)
20 }
21 if int(math.Abs(float64(want)-float64(have))) > tolerance {
22 t.Errorf("quantile %d: want %d, have %d", quantile, want, have)
23 }
24 }
25 }
+0
-63
metrics/teststat/graphite.go less more
0 package teststat
1
2 import (
3 "fmt"
4 "math"
5 "regexp"
6 "strconv"
7 "testing"
8 )
9
10 // AssertGraphiteNormalHistogram ensures the expvar Histogram referenced by
11 // metricName abides a normal distribution.
12 func AssertGraphiteNormalHistogram(t *testing.T, prefix, metricName string, mean, stdev int64, quantiles []int, gPayload string) {
13 // check for hdr histo data
14 wants := map[string]int64{"count": 1234, "min": 15, "max": 83}
15 for key, want := range wants {
16 re := regexp.MustCompile(fmt.Sprintf("%s%s.%s (\\d*)", prefix, metricName, key))
17 res := re.FindAllStringSubmatch(gPayload, 1)
18 if res == nil {
19 t.Error("did not find metrics log for", key, "in \n", gPayload)
20 continue
21 }
22
23 if len(res[0]) == 1 {
24 t.Fatalf("%q: bad regex, please check the test scenario", key)
25 }
26
27 have, err := strconv.ParseInt(res[0][1], 10, 64)
28 if err != nil {
29 t.Fatal(err)
30 }
31
32 if want != have {
33 t.Errorf("key %s: want %d, have %d", key, want, have)
34 }
35 }
36
37 const tolerance int = 2
38 wants = map[string]int64{".std-dev": stdev, ".mean": mean}
39 for _, quantile := range quantiles {
40 wants[fmt.Sprintf("_p%02d", quantile)] = normalValueAtQuantile(mean, stdev, quantile)
41 }
42 // check for quantile gauges
43 for key, want := range wants {
44 re := regexp.MustCompile(fmt.Sprintf("%s%s%s (\\d*\\.\\d*)", prefix, metricName, key))
45 res := re.FindAllStringSubmatch(gPayload, 1)
46 if res == nil {
47 t.Errorf("did not find metrics log for %s", key)
48 continue
49 }
50
51 if len(res[0]) == 1 {
52 t.Fatalf("%q: bad regex found, please check the test scenario", key)
53 }
54 have, err := strconv.ParseFloat(res[0][1], 64)
55 if err != nil {
56 t.Fatal(err)
57 }
58 if int(math.Abs(float64(want)-have)) > tolerance {
59 t.Errorf("key %s: want %.2f, have %.2f", key, want, have)
60 }
61 }
62 }
0 package teststat
1
2 import (
3 "math"
4 "math/rand"
5
6 "github.com/go-kit/kit/metrics3"
7 )
8
9 // PopulateNormalHistogram makes a series of normal random observations into the
10 // histogram. The number of observations is determined by Count. The randomness
11 // is determined by Mean, Stdev, and the seed parameter.
12 //
13 // This is a low-level function, exported only for metrics that don't perform
14 // dynamic quantile computation, like a Prometheus Histogram (c.f. Summary). In
15 // most cases, you don't need to use this function, and can use TestHistogram
16 // instead.
17 func PopulateNormalHistogram(h metrics.Histogram, seed int) {
18 r := rand.New(rand.NewSource(int64(seed)))
19 for i := 0; i < Count; i++ {
20 sample := r.NormFloat64()*float64(Stdev) + float64(Mean)
21 if sample < 0 {
22 sample = 0
23 }
24 h.Observe(sample)
25 }
26 }
27
28 func normalQuantiles() (p50, p90, p95, p99 float64) {
29 return nvq(50), nvq(90), nvq(95), nvq(99)
30 }
31
32 func nvq(quantile int) float64 {
33 // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
34 return float64(Mean) + float64(Stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)
35 }
36
37 func erfinv(y float64) float64 {
38 // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
39 if y < -1.0 || y > 1.0 {
40 panic("invalid input")
41 }
42
43 var (
44 a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
45 b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
46 c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
47 d = [2]float64{3.543889200, 1.637067800}
48 )
49
50 const y0 = 0.7
51 var x, z float64
52
53 if math.Abs(y) == 1.0 {
54 x = -y * math.Log(0.0)
55 } else if y < -y0 {
56 z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
57 x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
58 } else {
59 if y < y0 {
60 z = y * y
61 x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0)
62 } else {
63 z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
64 x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
65 }
66 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
67 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
68 }
69
70 return x
71 }
+0
-93
metrics/teststat/prometheus.go less more
0 package teststat
1
2 import (
3 "io/ioutil"
4 "math"
5 "net/http"
6 "net/http/httptest"
7 "regexp"
8 "strconv"
9 "strings"
10 "testing"
11
12 "github.com/prometheus/client_golang/prometheus"
13 )
14
15 // ScrapePrometheus returns the text encoding of the current state of
16 // Prometheus.
17 func ScrapePrometheus(t *testing.T) string {
18 server := httptest.NewServer(prometheus.UninstrumentedHandler())
19 defer server.Close()
20
21 resp, err := http.Get(server.URL)
22 if err != nil {
23 t.Fatal(err)
24 }
25 defer resp.Body.Close()
26
27 buf, err := ioutil.ReadAll(resp.Body)
28 if err != nil {
29 t.Fatal(err)
30 }
31
32 return strings.TrimSpace(string(buf))
33 }
34
35 // AssertPrometheusNormalSummary ensures the Prometheus Summary referenced by
36 // name abides a normal distribution.
37 func AssertPrometheusNormalSummary(t *testing.T, metricName string, mean, stdev int64) {
38 scrape := ScrapePrometheus(t)
39 const tolerance int = 5 // Prometheus approximates higher quantiles badly -_-;
40 for quantileInt, quantileStr := range map[int]string{50: "0.5", 90: "0.9", 99: "0.99"} {
41 want := normalValueAtQuantile(mean, stdev, quantileInt)
42 have := getPrometheusQuantile(t, scrape, metricName, quantileStr)
43 if int(math.Abs(float64(want)-float64(have))) > tolerance {
44 t.Errorf("%q: want %d, have %d", quantileStr, want, have)
45 }
46 }
47 }
48
49 // AssertPrometheusBucketedHistogram ensures the Prometheus Histogram
50 // referenced by name has observations in the expected quantity and bucket.
51 func AssertPrometheusBucketedHistogram(t *testing.T, metricName string, mean, stdev int64, buckets []float64) {
52 scrape := ScrapePrometheus(t)
53 const tolerance int = population / 50 // pretty coarse-grained
54 for _, bucket := range buckets {
55 want := observationsLessThan(mean, stdev, bucket, population)
56 have := getPrometheusLessThan(t, scrape, metricName, strconv.FormatFloat(bucket, 'f', 0, 64))
57 if int(math.Abs(float64(want)-float64(have))) > tolerance {
58 t.Errorf("%.0f: want %d, have %d", bucket, want, have)
59 }
60 }
61 }
62
63 func getPrometheusQuantile(t *testing.T, scrape, name, quantileStr string) int {
64 matches := regexp.MustCompile(name+`{quantile="`+quantileStr+`"} ([0-9]+)`).FindAllStringSubmatch(scrape, -1)
65 if len(matches) < 1 {
66 t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr)
67 }
68 if len(matches[0]) < 2 {
69 t.Fatalf("%q: quantile %q not found in scrape", name, quantileStr)
70 }
71 i, err := strconv.Atoi(matches[0][1])
72 if err != nil {
73 t.Fatal(err)
74 }
75 return i
76 }
77
78 func getPrometheusLessThan(t *testing.T, scrape, name, target string) int {
79 matches := regexp.MustCompile(name+`{le="`+target+`"} ([0-9]+)`).FindAllStringSubmatch(scrape, -1)
80 if len(matches) < 1 {
81 t.Logf(">>>\n%s\n", scrape)
82 t.Fatalf("%q: bucket %q not found in scrape", name, target)
83 }
84 if len(matches[0]) < 2 {
85 t.Fatalf("%q: bucket %q not found in scrape", name, target)
86 }
87 i, err := strconv.Atoi(matches[0][1])
88 if err != nil {
89 t.Fatal(err)
90 }
91 return i
92 }
0 // Package teststat provides helpers for testing metrics backends.
1 package teststat
2
3 import (
4 "errors"
5 "fmt"
6 "math"
7 "math/rand"
8 "strings"
9
10 "github.com/go-kit/kit/metrics3"
11 )
12
13 // TestCounter puts some deltas through the counter, and then calls the value
14 // func to check that the counter has the correct final value.
15 func TestCounter(counter metrics.Counter, value func() float64) error {
16 a := rand.Perm(100)
17 n := rand.Intn(len(a))
18
19 var want float64
20 for i := 0; i < n; i++ {
21 f := float64(a[i])
22 counter.Add(f)
23 want += f
24 }
25
26 if have := value(); want != have {
27 return fmt.Errorf("want %f, have %f", want, have)
28 }
29
30 return nil
31 }
32
33 // TestGauge puts some values through the gauge, and then calls the value func
34 // to check that the gauge has the correct final value.
35 func TestGauge(gauge metrics.Gauge, value func() float64) error {
36 a := rand.Perm(100)
37 n := rand.Intn(len(a))
38
39 var want float64
40 for i := 0; i < n; i++ {
41 f := float64(a[i])
42 gauge.Set(f)
43 want = f
44 }
45
46 if have := value(); want != have {
47 return fmt.Errorf("want %f, have %f", want, have)
48 }
49
50 return nil
51 }
52
53 // TestHistogram puts some observations through the histogram, and then calls
54 // the quantiles func to checks that the histogram has computed the correct
55 // quantiles within some tolerance
56 func TestHistogram(histogram metrics.Histogram, quantiles func() (p50, p90, p95, p99 float64), tolerance float64) error {
57 PopulateNormalHistogram(histogram, rand.Int())
58
59 want50, want90, want95, want99 := normalQuantiles()
60 have50, have90, have95, have99 := quantiles()
61
62 var errs []string
63 if want, have := want50, have50; !cmp(want, have, tolerance) {
64 errs = append(errs, fmt.Sprintf("p50: want %f, have %f", want, have))
65 }
66 if want, have := want90, have90; !cmp(want, have, tolerance) {
67 errs = append(errs, fmt.Sprintf("p90: want %f, have %f", want, have))
68 }
69 if want, have := want95, have95; !cmp(want, have, tolerance) {
70 errs = append(errs, fmt.Sprintf("p95: want %f, have %f", want, have))
71 }
72 if want, have := want99, have99; !cmp(want, have, tolerance) {
73 errs = append(errs, fmt.Sprintf("p99: want %f, have %f", want, have))
74 }
75 if len(errs) > 0 {
76 return errors.New(strings.Join(errs, "; "))
77 }
78
79 return nil
80 }
81
82 var (
83 Count = 12345
84 Mean = 500
85 Stdev = 25
86 )
87
88 // ExpectedObservationsLessThan returns the number of observations that should
89 // have a value less than or equal to the given value, given a normal
90 // distribution of observations described by Count, Mean, and Stdev.
91 func ExpectedObservationsLessThan(bucket int64) int64 {
92 // https://code.google.com/p/gostat/source/browse/stat/normal.go
93 cdf := ((1.0 / 2.0) * (1 + math.Erf((float64(bucket)-float64(Mean))/(float64(Stdev)*math.Sqrt2))))
94 return int64(cdf * float64(Count))
95 }
96
97 func cmp(want, have, tol float64) bool {
98 if (math.Abs(want-have) / want) > tol {
99 return false
100 }
101 return true
102 }
+0
-34
metrics/time_histogram.go less more
0 package metrics
1
2 import "time"
3
4 // TimeHistogram is a convenience wrapper for a Histogram of time.Durations.
5 type TimeHistogram interface {
6 With(Field) TimeHistogram
7 Observe(time.Duration)
8 }
9
10 type timeHistogram struct {
11 unit time.Duration
12 Histogram
13 }
14
15 // NewTimeHistogram returns a TimeHistogram wrapper around the passed
16 // Histogram, in units of unit.
17 func NewTimeHistogram(unit time.Duration, h Histogram) TimeHistogram {
18 return &timeHistogram{
19 unit: unit,
20 Histogram: h,
21 }
22 }
23
24 func (h *timeHistogram) With(f Field) TimeHistogram {
25 return &timeHistogram{
26 Histogram: h.Histogram.With(f),
27 unit: h.unit,
28 }
29 }
30
31 func (h *timeHistogram) Observe(d time.Duration) {
32 h.Histogram.Observe(int64(d / h.unit))
33 }
+0
-32
metrics/time_histogram_test.go less more
0 package metrics_test
1
2 import (
3 "math/rand"
4 "testing"
5 "time"
6
7 "github.com/go-kit/kit/metrics"
8 "github.com/go-kit/kit/metrics/expvar"
9 )
10
11 func TestTimeHistogram(t *testing.T) {
12 var (
13 metricName = "test_time_histogram"
14 minValue = int64(0)
15 maxValue = int64(200)
16 sigfigs = 3
17 quantiles = []int{50, 90, 99}
18 h = expvar.NewHistogram(metricName, minValue, maxValue, sigfigs, quantiles...)
19 th = metrics.NewTimeHistogram(time.Millisecond, h).With(metrics.Field{Key: "a", Value: "b"})
20 )
21
22 const seed, mean, stdev int64 = 321, 100, 20
23 r := rand.New(rand.NewSource(seed))
24
25 for i := 0; i < 4321; i++ {
26 sample := time.Duration(r.NormFloat64()*float64(stdev)+float64(mean)) * time.Millisecond
27 th.Observe(sample)
28 }
29
30 assertExpvarNormalHistogram(t, metricName, mean, stdev, quantiles)
31 }
+0
-83
metrics3/README.md less more
0 # package metrics
1
2 `package metrics` provides a set of uniform interfaces for service instrumentation.
3 It has
4 [counters](http://prometheus.io/docs/concepts/metric_types/#counter),
5 [gauges](http://prometheus.io/docs/concepts/metric_types/#gauge), and
6 [histograms](http://prometheus.io/docs/concepts/metric_types/#histogram),
7 and provides adapters to popular metrics packages, like
8 [expvar](https://golang.org/pkg/expvar),
9 [StatsD](https://github.com/etsy/statsd), and
10 [Prometheus](https://prometheus.io).
11
12 ## Rationale
13
14 Code instrumentation is absolutely essential to achieve
15 [observability](https://speakerdeck.com/mattheath/observability-in-micro-service-architectures)
16 into a distributed system.
17 Metrics and instrumentation tools have coalesced around a few well-defined idioms.
18 `package metrics` provides a common, minimal interface those idioms for service authors.
19
20 ## Usage
21
22 A simple counter, exported via expvar.
23
24 ```go
25 import "github.com/go-kit/kit/metrics/expvar"
26
27 func main() {
28 myCount := expvar.NewCounter("my_count")
29 myCount.Add(1)
30 }
31 ```
32
33 A histogram for request duration,
34 exported via a Prometheus summary with dynamically-computed quantiles.
35
36 ```go
37 import (
38 stdprometheus "github.com/prometheus/client_golang/prometheus"
39
40 "github.com/go-kit/kit/metrics"
41 "github.com/go-kit/kit/metrics/prometheus"
42 )
43
44 var dur = prometheus.NewSummary(stdprometheus.SummaryOpts{
45 Namespace: "myservice",
46 Subsystem: "api",
47 Name: "request_duration_seconds",
48 Help: "Total time spent serving requests.",
49 }, []string{})
50
51 func handleRequest() {
52 defer func(begin time.Time) { dur.Observe(time.Since(begin).Seconds()) }(time.Now())
53 // handle request
54 }
55 ```
56
57 A gauge for the number of goroutines currently running, exported via StatsD.
58
59 ```go
60 import (
61 "net"
62 "os"
63 "runtime"
64 "time"
65
66 "github.com/go-kit/kit/metrics/statsd"
67 "github.com/go-kit/kit/log"
68 )
69
70 func main() {
71 statsd := statsd.New("foo_svc.", log.NewNopLogger())
72
73 report := time.NewTicker(5*time.Second)
74 defer report.Stop()
75 go statsd.SendLoop(report.C, "tcp", "statsd.internal:8125")
76
77 goroutines := statsd.NewGauge("goroutine_count")
78 for range time.Tick(time.Second) {
79 goroutines.Set(float64(runtime.NumGoroutine()))
80 }
81 }
82 ```
+0
-85
metrics3/circonus/circonus.go less more
0 // Package circonus provides a Circonus backend for metrics.
1 package circonus
2
3 import (
4 "github.com/circonus-labs/circonus-gometrics"
5
6 "github.com/go-kit/kit/metrics3"
7 )
8
9 // Circonus wraps a CirconusMetrics object and provides constructors for each of
10 // the Go kit metrics. The CirconusMetrics object manages aggregation of
11 // observations and emission to the Circonus server.
12 type Circonus struct {
13 m *circonusgometrics.CirconusMetrics
14 }
15
16 // New creates a new Circonus object wrapping the passed CirconusMetrics, which
17 // the caller should create and set in motion. The Circonus object can be used
18 // to construct individual Go kit metrics.
19 func New(m *circonusgometrics.CirconusMetrics) *Circonus {
20 return &Circonus{
21 m: m,
22 }
23 }
24
25 // NewCounter returns a counter metric with the given name.
26 func (c *Circonus) NewCounter(name string) *Counter {
27 return &Counter{
28 name: name,
29 m: c.m,
30 }
31 }
32
33 // NewGauge returns a gauge metric with the given name.
34 func (c *Circonus) NewGauge(name string) *Gauge {
35 return &Gauge{
36 name: name,
37 m: c.m,
38 }
39 }
40
41 // NewHistogram returns a histogram metric with the given name.
42 func (c *Circonus) NewHistogram(name string) *Histogram {
43 return &Histogram{
44 h: c.m.NewHistogram(name),
45 }
46 }
47
48 // Counter is a Circonus implementation of a counter metric.
49 type Counter struct {
50 name string
51 m *circonusgometrics.CirconusMetrics
52 }
53
54 // With implements Counter, but is a no-op, because Circonus metrics have no
55 // concept of per-observation label values.
56 func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
57
58 // Add implements Counter. Delta is converted to uint64; precision will be lost.
59 func (c *Counter) Add(delta float64) { c.m.Add(c.name, uint64(delta)) }
60
61 // Gauge is a Circonus implementation of a gauge metric.
62 type Gauge struct {
63 name string
64 m *circonusgometrics.CirconusMetrics
65 }
66
67 // With implements Gauge, but is a no-op, because Circonus metrics have no
68 // concept of per-observation label values.
69 func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
70
71 // Set implements Gauge.
72 func (g *Gauge) Set(value float64) { g.m.SetGauge(g.name, value) }
73
74 // Histogram is a Circonus implementation of a histogram metric.
75 type Histogram struct {
76 h *circonusgometrics.Histogram
77 }
78
79 // With implements Histogram, but is a no-op, because Circonus metrics have no
80 // concept of per-observation label values.
81 func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
82
83 // Observe implements Histogram. No precision is lost.
84 func (h *Histogram) Observe(value float64) { h.h.RecordValue(value) }
+0
-120
metrics3/circonus/circonus_test.go less more
0 package circonus
1
2 import (
3 "encoding/json"
4 "net/http"
5 "net/http/httptest"
6 "regexp"
7 "strconv"
8 "testing"
9
10 "github.com/circonus-labs/circonus-gometrics"
11 "github.com/circonus-labs/circonus-gometrics/checkmgr"
12
13 "github.com/go-kit/kit/metrics3/generic"
14 "github.com/go-kit/kit/metrics3/teststat"
15 )
16
17 func TestCounter(t *testing.T) {
18 // The only way to extract values from Circonus is to pose as a Circonus
19 // server and receive real HTTP writes.
20 const name = "abc"
21 var val int64
22 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 var res map[string]struct {
24 Value int64 `json:"_value"` // reverse-engineered :\
25 }
26 json.NewDecoder(r.Body).Decode(&res)
27 val = res[name].Value
28 }))
29 defer s.Close()
30
31 // Set up a Circonus object, submitting to our HTTP server.
32 m := newCirconusMetrics(s.URL)
33 counter := New(m).NewCounter(name).With("label values", "not supported")
34 value := func() float64 { m.Flush(); return float64(val) }
35
36 // Engage.
37 if err := teststat.TestCounter(counter, value); err != nil {
38 t.Fatal(err)
39 }
40 }
41
42 func TestGauge(t *testing.T) {
43 const name = "def"
44 var val float64
45 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46 var res map[string]struct {
47 Value string `json:"_value"`
48 }
49 json.NewDecoder(r.Body).Decode(&res)
50 val, _ = strconv.ParseFloat(res[name].Value, 64)
51 }))
52 defer s.Close()
53
54 m := newCirconusMetrics(s.URL)
55 gauge := New(m).NewGauge(name).With("label values", "not supported")
56 value := func() float64 { m.Flush(); return val }
57
58 if err := teststat.TestGauge(gauge, value); err != nil {
59 t.Fatal(err)
60 }
61 }
62
63 func TestHistogram(t *testing.T) {
64 const name = "ghi"
65
66 // Circonus just emits bucketed counts. We'll dump them into a generic
67 // histogram (losing some precision) and take statistics from there. Note
68 // this does assume that the generic histogram computes statistics properly,
69 // but we have another test for that :)
70 re := regexp.MustCompile(`^H\[([0-9\.e\+]+)\]=([0-9]+)$`) // H[1.2e+03]=456
71
72 var p50, p90, p95, p99 float64
73 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74 var res map[string]struct {
75 Values []string `json:"_value"` // reverse-engineered :\
76 }
77 json.NewDecoder(r.Body).Decode(&res)
78
79 h := generic.NewHistogram("dummy", len(res[name].Values)) // match tbe bucket counts
80 for _, v := range res[name].Values {
81 match := re.FindStringSubmatch(v)
82 f, _ := strconv.ParseFloat(match[1], 64)
83 n, _ := strconv.ParseInt(match[2], 10, 64)
84 for i := int64(0); i < n; i++ {
85 h.Observe(f)
86 }
87 }
88
89 p50 = h.Quantile(0.50)
90 p90 = h.Quantile(0.90)
91 p95 = h.Quantile(0.95)
92 p99 = h.Quantile(0.99)
93 }))
94 defer s.Close()
95
96 m := newCirconusMetrics(s.URL)
97 histogram := New(m).NewHistogram(name).With("label values", "not supported")
98 quantiles := func() (float64, float64, float64, float64) { m.Flush(); return p50, p90, p95, p99 }
99
100 // Circonus metrics, because they do their own bucketing, are less precise
101 // than other systems. So, we bump the tolerance to 5 percent.
102 if err := teststat.TestHistogram(histogram, quantiles, 0.05); err != nil {
103 t.Fatal(err)
104 }
105 }
106
107 func newCirconusMetrics(url string) *circonusgometrics.CirconusMetrics {
108 m, err := circonusgometrics.NewCirconusMetrics(&circonusgometrics.Config{
109 CheckManager: checkmgr.Config{
110 Check: checkmgr.CheckConfig{
111 SubmissionURL: url,
112 },
113 },
114 })
115 if err != nil {
116 panic(err)
117 }
118 return m
119 }
+0
-37
metrics3/discard/discard.go less more
0 // Package discard provides a no-op metrics backend.
1 package discard
2
3 import "github.com/go-kit/kit/metrics3"
4
5 type counter struct{}
6
7 // NewCounter returns a new no-op counter.
8 func NewCounter() metrics.Counter { return counter{} }
9
10 // With implements Counter.
11 func (c counter) With(labelValues ...string) metrics.Counter { return c }
12
13 // Add implements Counter.
14 func (c counter) Add(delta float64) {}
15
16 type gauge struct{}
17
18 // NewGauge returns a new no-op gauge.
19 func NewGauge() metrics.Gauge { return gauge{} }
20
21 // With implements Gauge.
22 func (g gauge) With(labelValues ...string) metrics.Gauge { return g }
23
24 // Set implements Gauge.
25 func (g gauge) Set(value float64) {}
26
27 type histogram struct{}
28
29 // NewHistogram returns a new no-op histogram.
30 func NewHistogram() metrics.Histogram { return histogram{} }
31
32 // With implements Histogram.
33 func (h histogram) With(labelValues ...string) metrics.Histogram { return h }
34
35 // Observe implements histogram.
36 func (h histogram) Observe(value float64) {}
+0
-59
metrics3/doc.go less more
0 // Package metrics provides a framework for application instrumentation. All
1 // metrics are safe for concurrent use. Considerable design influence has been
2 // taken from https://github.com/codahale/metrics and https://prometheus.io.
3 //
4 // This package contains the common interfaces. Your code should take these
5 // interfaces as parameters. Implementations are provided for different
6 // instrumentation systems in the various subdirectories.
7 //
8 // Usage
9 //
10 // Metrics are dependencies and should be passed to the components that need
11 // them in the same way you'd construct and pass a database handle, or reference
12 // to another component. So, create metrics in your func main, using whichever
13 // concrete implementation is appropriate for your organization.
14 //
15 // latency := prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
16 // Namespace: "myteam",
17 // Subsystem: "foosvc",
18 // Name: "request_latency_seconds",
19 // Help: "Incoming request latency in seconds."
20 // }, []string{"method", "status_code"})
21 //
22 // Write your components to take the metrics they will use as parameters to
23 // their constructors. Use the interface types, not the concrete types. That is,
24 //
25 // // NewAPI takes metrics.Histogram, not *prometheus.Summary
26 // func NewAPI(s Store, logger log.Logger, latency metrics.Histogram) *API {
27 // // ...
28 // }
29 //
30 // func (a *API) ServeFoo(w http.ResponseWriter, r *http.Request) {
31 // begin := time.Now()
32 // // ...
33 // a.latency.Observe(time.Since(begin).Seconds())
34 // }
35 //
36 // Finally, pass the metrics as dependencies when building your object graph.
37 // This should happen in func main, not in the global scope.
38 //
39 // api := NewAPI(store, logger, latency)
40 // http.ListenAndServe("/", api)
41 //
42 // Implementation details
43 //
44 // Each telemetry system has different semantics for label values, push vs.
45 // pull, support for histograms, etc. These properties influence the design of
46 // their respective packages. This table attempts to summarize the key points of
47 // distinction.
48 //
49 // SYSTEM DIM COUNTERS GAUGES HISTOGRAMS
50 // dogstatsd n batch, push-aggregate batch, push-aggregate native, batch, push-each
51 // statsd 1 batch, push-aggregate batch, push-aggregate native, batch, push-each
52 // graphite 1 batch, push-aggregate batch, push-aggregate synthetic, batch, push-aggregate
53 // expvar 1 atomic atomic synthetic, batch, in-place expose
54 // influx n custom custom custom
55 // prometheus n native native native
56 // circonus 1 native native native
57 //
58 package metrics
+0
-306
metrics3/dogstatsd/dogstatsd.go less more
0 // Package dogstatsd provides a DogStatsD backend for package metrics. It's very
1 // similar to StatsD, but supports arbitrary tags per-metric, which map to Go
2 // kit's label values. So, while label values are no-ops in StatsD, they are
3 // supported here. For more details, see the documentation at
4 // http://docs.datadoghq.com/guides/dogstatsd/.
5 //
6 // This package batches observations and emits them on some schedule to the
7 // remote server. This is useful even if you connect to your DogStatsD server
8 // over UDP. Emitting one network packet per observation can quickly overwhelm
9 // even the fastest internal network.
10 package dogstatsd
11
12 import (
13 "fmt"
14 "io"
15 "strings"
16 "time"
17
18 "github.com/go-kit/kit/log"
19 "github.com/go-kit/kit/metrics3"
20 "github.com/go-kit/kit/metrics3/internal/lv"
21 "github.com/go-kit/kit/metrics3/internal/ratemap"
22 "github.com/go-kit/kit/util/conn"
23 )
24
25 // Dogstatsd receives metrics observations and forwards them to a DogStatsD
26 // server. Create a Dogstatsd object, use it to create metrics, and pass those
27 // metrics as dependencies to the components that will use them.
28 //
29 // All metrics are buffered until WriteTo is called. Counters and gauges are
30 // aggregated into a single observation per timeseries per write. Timings and
31 // histograms are buffered but not aggregated.
32 //
33 // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
34 // To send to a DogStatsD server, use the SendLoop helper method.
35 type Dogstatsd struct {
36 prefix string
37 rates *ratemap.RateMap
38 counters *lv.Space
39 gauges *lv.Space
40 timings *lv.Space
41 histograms *lv.Space
42 logger log.Logger
43 }
44
45 // New returns a Dogstatsd object that may be used to create metrics. Prefix is
46 // applied to all created metrics. Callers must ensure that regular calls to
47 // WriteTo are performed, either manually or with one of the helper methods.
48 func New(prefix string, logger log.Logger) *Dogstatsd {
49 return &Dogstatsd{
50 prefix: prefix,
51 rates: ratemap.New(),
52 counters: lv.NewSpace(),
53 gauges: lv.NewSpace(),
54 timings: lv.NewSpace(),
55 histograms: lv.NewSpace(),
56 logger: logger,
57 }
58 }
59
60 // NewCounter returns a counter, sending observations to this Dogstatsd object.
61 func (d *Dogstatsd) NewCounter(name string, sampleRate float64) *Counter {
62 d.rates.Set(d.prefix+name, sampleRate)
63 return &Counter{
64 name: d.prefix + name,
65 obs: d.counters.Observe,
66 }
67 }
68
69 // NewGauge returns a gauge, sending observations to this Dogstatsd object.
70 func (d *Dogstatsd) NewGauge(name string) *Gauge {
71 return &Gauge{
72 name: d.prefix + name,
73 obs: d.gauges.Observe,
74 }
75 }
76
77 // NewTiming returns a histogram whose observations are interpreted as
78 // millisecond durations, and are forwarded to this Dogstatsd object.
79 func (d *Dogstatsd) NewTiming(name string, sampleRate float64) *Timing {
80 d.rates.Set(d.prefix+name, sampleRate)
81 return &Timing{
82 name: d.prefix + name,
83 obs: d.timings.Observe,
84 }
85 }
86
87 // NewHistogram returns a histogram whose observations are of an unspecified
88 // unit, and are forwarded to this Dogstatsd object.
89 func (d *Dogstatsd) NewHistogram(name string, sampleRate float64) *Histogram {
90 d.rates.Set(d.prefix+name, sampleRate)
91 return &Histogram{
92 name: d.prefix + name,
93 obs: d.histograms.Observe,
94 }
95 }
96
97 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
98 // time the passed channel fires. This method blocks until the channel is
99 // closed, so clients probably want to run it in its own goroutine. For typical
100 // usage, create a time.Ticker and pass its C channel to this method.
101 func (d *Dogstatsd) WriteLoop(c <-chan time.Time, w io.Writer) {
102 for range c {
103 if _, err := d.WriteTo(w); err != nil {
104 d.logger.Log("during", "WriteTo", "err", err)
105 }
106 }
107 }
108
109 // SendLoop is a helper method that wraps WriteLoop, passing a managed
110 // connection to the network and address. Like WriteLoop, this method blocks
111 // until the channel is closed, so clients probably want to start it in its own
112 // goroutine. For typical usage, create a time.Ticker and pass its C channel to
113 // this method.
114 func (d *Dogstatsd) SendLoop(c <-chan time.Time, network, address string) {
115 d.WriteLoop(c, conn.NewDefaultManager(network, address, d.logger))
116 }
117
118 // WriteTo flushes the buffered content of the metrics to the writer, in
119 // DogStatsD format. WriteTo abides best-effort semantics, so observations are
120 // lost if there is a problem with the write. Clients should be sure to call
121 // WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
122 func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) {
123 var n int
124
125 d.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
126 n, err = fmt.Fprintf(w, "%s:%f|c%s%s\n", name, sum(values), sampling(d.rates.Get(name)), tagValues(lvs))
127 if err != nil {
128 return false
129 }
130 count += int64(n)
131 return true
132 })
133 if err != nil {
134 return count, err
135 }
136
137 d.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
138 n, err = fmt.Fprintf(w, "%s:%f|g%s\n", name, last(values), tagValues(lvs))
139 if err != nil {
140 return false
141 }
142 count += int64(n)
143 return true
144 })
145 if err != nil {
146 return count, err
147 }
148
149 d.timings.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
150 sampleRate := d.rates.Get(name)
151 for _, value := range values {
152 n, err = fmt.Fprintf(w, "%s:%f|ms%s%s\n", name, value, sampling(sampleRate), tagValues(lvs))
153 if err != nil {
154 return false
155 }
156 count += int64(n)
157 }
158 return true
159 })
160 if err != nil {
161 return count, err
162 }
163
164 d.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
165 sampleRate := d.rates.Get(name)
166 for _, value := range values {
167 n, err = fmt.Fprintf(w, "%s:%f|h%s%s\n", name, value, sampling(sampleRate), tagValues(lvs))
168 if err != nil {
169 return false
170 }
171 count += int64(n)
172 }
173 return true
174 })
175 if err != nil {
176 return count, err
177 }
178
179 return count, err
180 }
181
182 func sum(a []float64) float64 {
183 var v float64
184 for _, f := range a {
185 v += f
186 }
187 return v
188 }
189
190 func last(a []float64) float64 {
191 return a[len(a)-1]
192 }
193
194 func sampling(r float64) string {
195 var sv string
196 if r < 1.0 {
197 sv = fmt.Sprintf("|@%f", r)
198 }
199 return sv
200 }
201
202 func tagValues(labelValues []string) string {
203 if len(labelValues) == 0 {
204 return ""
205 }
206 if len(labelValues)%2 != 0 {
207 panic("tagValues received a labelValues with an odd number of strings")
208 }
209 pairs := make([]string, 0, len(labelValues)/2)
210 for i := 0; i < len(labelValues); i += 2 {
211 pairs = append(pairs, labelValues[i]+":"+labelValues[i+1])
212 }
213 return "|#" + strings.Join(pairs, ",")
214 }
215
216 type observeFunc func(name string, lvs lv.LabelValues, value float64)
217
218 // Counter is a DogStatsD counter. Observations are forwarded to a Dogstatsd
219 // object, and aggregated (summed) per timeseries.
220 type Counter struct {
221 name string
222 lvs lv.LabelValues
223 obs observeFunc
224 }
225
226 // With implements metrics.Counter.
227 func (c *Counter) With(labelValues ...string) metrics.Counter {
228 return &Counter{
229 name: c.name,
230 lvs: c.lvs.With(labelValues...),
231 obs: c.obs,
232 }
233 }
234
235 // Add implements metrics.Counter.
236 func (c *Counter) Add(delta float64) {
237 c.obs(c.name, c.lvs, delta)
238 }
239
240 // Gauge is a DogStatsD gauge. Observations are forwarded to a Dogstatsd
241 // object, and aggregated (the last observation selected) per timeseries.
242 type Gauge struct {
243 name string
244 lvs lv.LabelValues
245 obs observeFunc
246 }
247
248 // With implements metrics.Gauge.
249 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
250 return &Gauge{
251 name: g.name,
252 lvs: g.lvs.With(labelValues...),
253 obs: g.obs,
254 }
255 }
256
257 // Set implements metrics.Gauge.
258 func (g *Gauge) Set(value float64) {
259 g.obs(g.name, g.lvs, value)
260 }
261
262 // Timing is a DogStatsD timing, or metrics.Histogram. Observations are
263 // forwarded to a Dogstatsd object, and collected (but not aggregated) per
264 // timeseries.
265 type Timing struct {
266 name string
267 lvs lv.LabelValues
268 obs observeFunc
269 }
270
271 // With implements metrics.Timing.
272 func (t *Timing) With(labelValues ...string) metrics.Histogram {
273 return &Timing{
274 name: t.name,
275 lvs: t.lvs.With(labelValues...),
276 obs: t.obs,
277 }
278 }
279
280 // Observe implements metrics.Histogram. Value is interpreted as milliseconds.
281 func (t *Timing) Observe(value float64) {
282 t.obs(t.name, t.lvs, value)
283 }
284
285 // Histogram is a DogStatsD histrogram. Observations are forwarded to a
286 // Dogstatsd object, and collected (but not aggregated) per timeseries.
287 type Histogram struct {
288 name string
289 lvs lv.LabelValues
290 obs observeFunc
291 }
292
293 // With implements metrics.Histogram.
294 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
295 return &Histogram{
296 name: h.name,
297 lvs: h.lvs.With(labelValues...),
298 obs: h.obs,
299 }
300 }
301
302 // Observe implements metrics.Histogram.
303 func (h *Histogram) Observe(value float64) {
304 h.obs(h.name, h.lvs, value)
305 }
+0
-90
metrics3/dogstatsd/dogstatsd_test.go less more
0 package dogstatsd
1
2 import (
3 "testing"
4
5 "github.com/go-kit/kit/log"
6 "github.com/go-kit/kit/metrics3/teststat"
7 )
8
9 func TestCounter(t *testing.T) {
10 prefix, name := "abc.", "def"
11 label, value := "label", "value"
12 regex := `^` + prefix + name + `:([0-9\.]+)\|c\|#` + label + `:` + value + `$`
13 d := New(prefix, log.NewNopLogger())
14 counter := d.NewCounter(name, 1.0).With(label, value)
15 valuef := teststat.SumLines(d, regex)
16 if err := teststat.TestCounter(counter, valuef); err != nil {
17 t.Fatal(err)
18 }
19 }
20
21 func TestCounterSampled(t *testing.T) {
22 // This will involve multiplying the observed sum by the inverse of the
23 // sample rate and checking against the expected value within some
24 // tolerance.
25 t.Skip("TODO")
26 }
27
28 func TestGauge(t *testing.T) {
29 prefix, name := "ghi.", "jkl"
30 label, value := "xyz", "abc"
31 regex := `^` + prefix + name + `:([0-9\.]+)\|g\|#` + label + `:` + value + `$`
32 d := New(prefix, log.NewNopLogger())
33 gauge := d.NewGauge(name).With(label, value)
34 valuef := teststat.LastLine(d, regex)
35 if err := teststat.TestGauge(gauge, valuef); err != nil {
36 t.Fatal(err)
37 }
38 }
39
40 // DogStatsD histograms just emit all observations. So, we collect them into
41 // a generic histogram, and run the statistics test on that.
42
43 func TestHistogram(t *testing.T) {
44 prefix, name := "dogstatsd.", "histogram_test"
45 label, value := "abc", "def"
46 regex := `^` + prefix + name + `:([0-9\.]+)\|h\|#` + label + `:` + value + `$`
47 d := New(prefix, log.NewNopLogger())
48 histogram := d.NewHistogram(name, 1.0).With(label, value)
49 quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
50 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
51 t.Fatal(err)
52 }
53 }
54
55 func TestHistogramSampled(t *testing.T) {
56 prefix, name := "dogstatsd.", "sampled_histogram_test"
57 label, value := "foo", "bar"
58 regex := `^` + prefix + name + `:([0-9\.]+)\|h\|@0\.01[0]*\|#` + label + `:` + value + `$`
59 d := New(prefix, log.NewNopLogger())
60 histogram := d.NewHistogram(name, 0.01).With(label, value)
61 quantiles := teststat.Quantiles(d, regex, 50)
62 if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
63 t.Fatal(err)
64 }
65 }
66
67 func TestTiming(t *testing.T) {
68 prefix, name := "dogstatsd.", "timing_test"
69 label, value := "wiggle", "bottom"
70 regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|#` + label + `:` + value + `$`
71 d := New(prefix, log.NewNopLogger())
72 histogram := d.NewTiming(name, 1.0).With(label, value)
73 quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
74 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
75 t.Fatal(err)
76 }
77 }
78
79 func TestTimingSampled(t *testing.T) {
80 prefix, name := "dogstatsd.", "sampled_timing_test"
81 label, value := "internal", "external"
82 regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0.03[0]*\|#` + label + `:` + value + `$`
83 d := New(prefix, log.NewNopLogger())
84 histogram := d.NewTiming(name, 0.03).With(label, value)
85 quantiles := teststat.Quantiles(d, regex, 50)
86 if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
87 t.Fatal(err)
88 }
89 }
+0
-91
metrics3/expvar/expvar.go less more
0 // Package expvar provides expvar backends for metrics.
1 // Label values are not supported.
2 package expvar
3
4 import (
5 "expvar"
6 "sync"
7
8 "github.com/go-kit/kit/metrics3"
9 "github.com/go-kit/kit/metrics3/generic"
10 )
11
12 // Counter implements the counter metric with an expvar float.
13 // Label values are not supported.
14 type Counter struct {
15 f *expvar.Float
16 }
17
18 // NewCounter creates an expvar Float with the given name, and returns an object
19 // that implements the Counter interface.
20 func NewCounter(name string) *Counter {
21 return &Counter{
22 f: expvar.NewFloat(name),
23 }
24 }
25
26 // With is a no-op.
27 func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
28
29 // Add implements Counter.
30 func (c *Counter) Add(delta float64) { c.f.Add(delta) }
31
32 // Gauge implements the gauge metric wtih an expvar float.
33 // Label values are not supported.
34 type Gauge struct {
35 f *expvar.Float
36 }
37
38 // NewGauge creates an expvar Float with the given name, and returns an object
39 // that implements the Gauge interface.
40 func NewGauge(name string) *Gauge {
41 return &Gauge{
42 f: expvar.NewFloat(name),
43 }
44 }
45
46 // With is a no-op.
47 func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
48
49 // Set implements Gauge.
50 func (g *Gauge) Set(value float64) { g.f.Set(value) }
51
52 // Histogram implements the histogram metric with a combination of the generic
53 // Histogram object and several expvar Floats, one for each of the 50th, 90th,
54 // 95th, and 99th quantiles of observed values, with the quantile attached to
55 // the name as a suffix. Label values are not supported.
56 type Histogram struct {
57 mtx sync.Mutex
58 h *generic.Histogram
59 p50 *expvar.Float
60 p90 *expvar.Float
61 p95 *expvar.Float
62 p99 *expvar.Float
63 }
64
65 // NewHistogram returns a Histogram object with the given name and number of
66 // buckets in the underlying histogram object. 50 is a good default number of
67 // buckets.
68 func NewHistogram(name string, buckets int) *Histogram {
69 return &Histogram{
70 h: generic.NewHistogram(name, buckets),
71 p50: expvar.NewFloat(name + ".p50"),
72 p90: expvar.NewFloat(name + ".p90"),
73 p95: expvar.NewFloat(name + ".p95"),
74 p99: expvar.NewFloat(name + ".p99"),
75 }
76 }
77
78 // With is a no-op.
79 func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
80
81 // Observe impleemts Histogram.
82 func (h *Histogram) Observe(value float64) {
83 h.mtx.Lock()
84 defer h.mtx.Unlock()
85 h.h.Observe(value)
86 h.p50.Set(h.h.Quantile(0.50))
87 h.p90.Set(h.h.Quantile(0.90))
88 h.p95.Set(h.h.Quantile(0.95))
89 h.p99.Set(h.h.Quantile(0.99))
90 }
+0
-38
metrics3/expvar/expvar_test.go less more
0 package expvar
1
2 import (
3 "strconv"
4 "testing"
5
6 "github.com/go-kit/kit/metrics3/teststat"
7 )
8
9 func TestCounter(t *testing.T) {
10 counter := NewCounter("expvar_counter").With("label values", "not supported").(*Counter)
11 value := func() float64 { f, _ := strconv.ParseFloat(counter.f.String(), 64); return f }
12 if err := teststat.TestCounter(counter, value); err != nil {
13 t.Fatal(err)
14 }
15 }
16
17 func TestGauge(t *testing.T) {
18 gauge := NewGauge("expvar_gauge").With("label values", "not supported").(*Gauge)
19 value := func() float64 { f, _ := strconv.ParseFloat(gauge.f.String(), 64); return f }
20 if err := teststat.TestGauge(gauge, value); err != nil {
21 t.Fatal(err)
22 }
23 }
24
25 func TestHistogram(t *testing.T) {
26 histogram := NewHistogram("expvar_histogram", 50).With("label values", "not supported").(*Histogram)
27 quantiles := func() (float64, float64, float64, float64) {
28 p50, _ := strconv.ParseFloat(histogram.p50.String(), 64)
29 p90, _ := strconv.ParseFloat(histogram.p90.String(), 64)
30 p95, _ := strconv.ParseFloat(histogram.p95.String(), 64)
31 p99, _ := strconv.ParseFloat(histogram.p99.String(), 64)
32 return p50, p90, p95, p99
33 }
34 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
35 t.Fatal(err)
36 }
37 }
+0
-218
metrics3/generic/generic.go less more
0 // Package generic implements generic versions of each of the metric types. They
1 // can be embedded by other implementations, and converted to specific formats
2 // as necessary.
3 package generic
4
5 import (
6 "fmt"
7 "io"
8 "math"
9 "sync"
10 "sync/atomic"
11
12 "github.com/VividCortex/gohistogram"
13
14 "github.com/go-kit/kit/metrics3"
15 "github.com/go-kit/kit/metrics3/internal/lv"
16 )
17
18 // Counter is an in-memory implementation of a Counter.
19 type Counter struct {
20 Name string
21 lvs lv.LabelValues
22 bits uint64
23 }
24
25 // NewCounter returns a new, usable Counter.
26 func NewCounter(name string) *Counter {
27 return &Counter{
28 Name: name,
29 }
30 }
31
32 // With implements Counter.
33 func (c *Counter) With(labelValues ...string) metrics.Counter {
34 return &Counter{
35 bits: atomic.LoadUint64(&c.bits),
36 lvs: c.lvs.With(labelValues...),
37 }
38 }
39
40 // Add implements Counter.
41 func (c *Counter) Add(delta float64) {
42 for {
43 var (
44 old = atomic.LoadUint64(&c.bits)
45 newf = math.Float64frombits(old) + delta
46 new = math.Float64bits(newf)
47 )
48 if atomic.CompareAndSwapUint64(&c.bits, old, new) {
49 break
50 }
51 }
52 }
53
54 // Value returns the current value of the counter.
55 func (c *Counter) Value() float64 {
56 return math.Float64frombits(atomic.LoadUint64(&c.bits))
57 }
58
59 // ValueReset returns the current value of the counter, and resets it to zero.
60 // This is useful for metrics backends whose counter aggregations expect deltas,
61 // like Graphite.
62 func (c *Counter) ValueReset() float64 {
63 for {
64 var (
65 old = atomic.LoadUint64(&c.bits)
66 newf = 0.0
67 new = math.Float64bits(newf)
68 )
69 if atomic.CompareAndSwapUint64(&c.bits, old, new) {
70 return math.Float64frombits(old)
71 }
72 }
73 }
74
75 // LabelValues returns the set of label values attached to the counter.
76 func (c *Counter) LabelValues() []string {
77 return c.lvs
78 }
79
80 // Gauge is an in-memory implementation of a Gauge.
81 type Gauge struct {
82 Name string
83 lvs lv.LabelValues
84 bits uint64
85 }
86
87 // NewGauge returns a new, usable Gauge.
88 func NewGauge(name string) *Gauge {
89 return &Gauge{
90 Name: name,
91 }
92 }
93
94 // With implements Gauge.
95 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
96 return &Gauge{
97 bits: atomic.LoadUint64(&g.bits),
98 lvs: g.lvs.With(labelValues...),
99 }
100 }
101
102 // Set implements Gauge.
103 func (g *Gauge) Set(value float64) {
104 atomic.StoreUint64(&g.bits, math.Float64bits(value))
105 }
106
107 // Value returns the current value of the gauge.
108 func (g *Gauge) Value() float64 {
109 return math.Float64frombits(atomic.LoadUint64(&g.bits))
110 }
111
112 // LabelValues returns the set of label values attached to the gauge.
113 func (g *Gauge) LabelValues() []string {
114 return g.lvs
115 }
116
117 // Histogram is an in-memory implementation of a streaming histogram, based on
118 // VividCortex/gohistogram. It dynamically computes quantiles, so it's not
119 // suitable for aggregation.
120 type Histogram struct {
121 Name string
122 lvs lv.LabelValues
123 h gohistogram.Histogram
124 }
125
126 // NewHistogram returns a numeric histogram based on VividCortex/gohistogram. A
127 // good default value for buckets is 50.
128 func NewHistogram(name string, buckets int) *Histogram {
129 return &Histogram{
130 Name: name,
131 h: gohistogram.NewHistogram(buckets),
132 }
133 }
134
135 // With implements Histogram.
136 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
137 return &Histogram{
138 lvs: h.lvs.With(labelValues...),
139 h: h.h,
140 }
141 }
142
143 // Observe implements Histogram.
144 func (h *Histogram) Observe(value float64) {
145 h.h.Add(value)
146 }
147
148 // Quantile returns the value of the quantile q, 0.0 < q < 1.0.
149 func (h *Histogram) Quantile(q float64) float64 {
150 return h.h.Quantile(q)
151 }
152
153 // LabelValues returns the set of label values attached to the histogram.
154 func (h *Histogram) LabelValues() []string {
155 return h.lvs
156 }
157
158 // Print writes a string representation of the histogram to the passed writer.
159 // Useful for printing to a terminal.
160 func (h *Histogram) Print(w io.Writer) {
161 fmt.Fprintf(w, h.h.String())
162 }
163
164 // Bucket is a range in a histogram which aggregates observations.
165 type Bucket struct {
166 From, To, Count int64
167 }
168
169 // Quantile is a pair of a quantile (0..100) and its observed maximum value.
170 type Quantile struct {
171 Quantile int // 0..100
172 Value int64
173 }
174
175 // SimpleHistogram is an in-memory implementation of a Histogram. It only tracks
176 // an approximate moving average, so is likely too naïve for many use cases.
177 type SimpleHistogram struct {
178 mtx sync.RWMutex
179 lvs lv.LabelValues
180 avg float64
181 n uint64
182 }
183
184 // NewSimpleHistogram returns a SimpleHistogram, ready for observations.
185 func NewSimpleHistogram() *SimpleHistogram {
186 return &SimpleHistogram{}
187 }
188
189 // With implements Histogram.
190 func (h *SimpleHistogram) With(labelValues ...string) metrics.Histogram {
191 return &SimpleHistogram{
192 lvs: h.lvs.With(labelValues...),
193 avg: h.avg,
194 n: h.n,
195 }
196 }
197
198 // Observe implements Histogram.
199 func (h *SimpleHistogram) Observe(value float64) {
200 h.mtx.Lock()
201 defer h.mtx.Unlock()
202 h.n++
203 h.avg -= h.avg / float64(h.n)
204 h.avg += value / float64(h.n)
205 }
206
207 // ApproximateMovingAverage returns the approximate moving average of observations.
208 func (h *SimpleHistogram) ApproximateMovingAverage() float64 {
209 h.mtx.RLock()
210 h.mtx.RUnlock()
211 return h.avg
212 }
213
214 // LabelValues returns the set of label values attached to the histogram.
215 func (h *SimpleHistogram) LabelValues() []string {
216 return h.lvs
217 }
+0
-75
metrics3/generic/generic_test.go less more
0 package generic_test
1
2 // This is package generic_test in order to get around an import cycle: this
3 // package imports teststat to do its testing, but package teststat imports
4 // generic to use its Histogram in the Quantiles helper function.
5
6 import (
7 "math"
8 "math/rand"
9 "testing"
10
11 "github.com/go-kit/kit/metrics3/generic"
12 "github.com/go-kit/kit/metrics3/teststat"
13 )
14
15 func TestCounter(t *testing.T) {
16 counter := generic.NewCounter("my_counter").With("label", "counter").(*generic.Counter)
17 value := func() float64 { return counter.Value() }
18 if err := teststat.TestCounter(counter, value); err != nil {
19 t.Fatal(err)
20 }
21 }
22
23 func TestValueReset(t *testing.T) {
24 counter := generic.NewCounter("test_value_reset")
25 counter.Add(123)
26 counter.Add(456)
27 counter.Add(789)
28 if want, have := float64(123+456+789), counter.ValueReset(); want != have {
29 t.Errorf("want %f, have %f", want, have)
30 }
31 if want, have := float64(0), counter.Value(); want != have {
32 t.Errorf("want %f, have %f", want, have)
33 }
34 }
35
36 func TestGauge(t *testing.T) {
37 gauge := generic.NewGauge("my_gauge").With("label", "gauge").(*generic.Gauge)
38 value := func() float64 { return gauge.Value() }
39 if err := teststat.TestGauge(gauge, value); err != nil {
40 t.Fatal(err)
41 }
42 }
43
44 func TestHistogram(t *testing.T) {
45 histogram := generic.NewHistogram("my_histogram", 50).With("label", "histogram").(*generic.Histogram)
46 quantiles := func() (float64, float64, float64, float64) {
47 return histogram.Quantile(0.50), histogram.Quantile(0.90), histogram.Quantile(0.95), histogram.Quantile(0.99)
48 }
49 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
50 t.Fatal(err)
51 }
52 }
53
54 func TestSimpleHistogram(t *testing.T) {
55 histogram := generic.NewSimpleHistogram().With("label", "simple_histogram").(*generic.SimpleHistogram)
56 var (
57 sum int
58 count = 1234 // not too big
59 )
60 for i := 0; i < count; i++ {
61 value := rand.Intn(1000)
62 sum += value
63 histogram.Observe(float64(value))
64 }
65
66 var (
67 want = float64(sum) / float64(count)
68 have = histogram.ApproximateMovingAverage()
69 tolerance = 0.001 // real real slim
70 )
71 if math.Abs(want-have)/want > tolerance {
72 t.Errorf("want %f, have %f", want, have)
73 }
74 }
+0
-200
metrics3/graphite/graphite.go less more
0 // Package graphite provides a Graphite backend for metrics. Metrics are batched
1 // and emitted in the plaintext protocol. For more information, see
2 // http://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol
3 //
4 // Graphite does not have a native understanding of metric parameterization, so
5 // label values not supported. Use distinct metrics for each unique combination
6 // of label values.
7 package graphite
8
9 import (
10 "fmt"
11 "io"
12 "sync"
13 "time"
14
15 "github.com/go-kit/kit/log"
16 "github.com/go-kit/kit/metrics3"
17 "github.com/go-kit/kit/metrics3/generic"
18 "github.com/go-kit/kit/util/conn"
19 )
20
21 // Graphite receives metrics observations and forwards them to a Graphite server.
22 // Create a Graphite object, use it to create metrics, and pass those metrics as
23 // dependencies to the components that will use them.
24 //
25 // All metrics are buffered until WriteTo is called. Counters and gauges are
26 // aggregated into a single observation per timeseries per write. Histograms are
27 // exploded into per-quantile gauges and reported once per write.
28 //
29 // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
30 // To send to a Graphite server, use the SendLoop helper method.
31 type Graphite struct {
32 mtx sync.RWMutex
33 prefix string
34 counters map[string]*Counter
35 gauges map[string]*Gauge
36 histograms map[string]*Histogram
37 logger log.Logger
38 }
39
40 // New returns a Statsd object that may be used to create metrics. Prefix is
41 // applied to all created metrics. Callers must ensure that regular calls to
42 // WriteTo are performed, either manually or with one of the helper methods.
43 func New(prefix string, logger log.Logger) *Graphite {
44 return &Graphite{
45 prefix: prefix,
46 counters: map[string]*Counter{},
47 gauges: map[string]*Gauge{},
48 histograms: map[string]*Histogram{},
49 logger: logger,
50 }
51 }
52
53 // NewCounter returns a counter. Observations are aggregated and emitted once
54 // per write invocation.
55 func (g *Graphite) NewCounter(name string) *Counter {
56 c := NewCounter(g.prefix + name)
57 g.mtx.Lock()
58 g.counters[g.prefix+name] = c
59 g.mtx.Unlock()
60 return c
61 }
62
63 // NewGauge returns a gauge. Observations are aggregated and emitted once per
64 // write invocation.
65 func (g *Graphite) NewGauge(name string) *Gauge {
66 ga := NewGauge(g.prefix + name)
67 g.mtx.Lock()
68 g.gauges[g.prefix+name] = ga
69 g.mtx.Unlock()
70 return ga
71 }
72
73 // NewHistogram returns a histogram. Observations are aggregated and emitted as
74 // per-quantile gauges, once per write invocation. 50 is a good default value
75 // for buckets.
76 func (g *Graphite) NewHistogram(name string, buckets int) *Histogram {
77 h := NewHistogram(g.prefix+name, buckets)
78 g.mtx.Lock()
79 g.histograms[g.prefix+name] = h
80 g.mtx.Unlock()
81 return h
82 }
83
84 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
85 // time the passed channel fires. This method blocks until the channel is
86 // closed, so clients probably want to run it in its own goroutine. For typical
87 // usage, create a time.Ticker and pass its C channel to this method.
88 func (g *Graphite) WriteLoop(c <-chan time.Time, w io.Writer) {
89 for range c {
90 if _, err := g.WriteTo(w); err != nil {
91 g.logger.Log("during", "WriteTo", "err", err)
92 }
93 }
94 }
95
96 // SendLoop is a helper method that wraps WriteLoop, passing a managed
97 // connection to the network and address. Like WriteLoop, this method blocks
98 // until the channel is closed, so clients probably want to start it in its own
99 // goroutine. For typical usage, create a time.Ticker and pass its C channel to
100 // this method.
101 func (g *Graphite) SendLoop(c <-chan time.Time, network, address string) {
102 g.WriteLoop(c, conn.NewDefaultManager(network, address, g.logger))
103 }
104
105 // WriteTo flushes the buffered content of the metrics to the writer, in
106 // Graphite plaintext format. WriteTo abides best-effort semantics, so
107 // observations are lost if there is a problem with the write. Clients should be
108 // sure to call WriteTo regularly, ideally through the WriteLoop or SendLoop
109 // helper methods.
110 func (g *Graphite) WriteTo(w io.Writer) (count int64, err error) {
111 g.mtx.RLock()
112 defer g.mtx.RUnlock()
113 now := time.Now().Unix()
114
115 for name, c := range g.counters {
116 n, err := fmt.Fprintf(w, "%s %f %d\n", name, c.c.ValueReset(), now)
117 if err != nil {
118 return count, err
119 }
120 count += int64(n)
121 }
122
123 for name, ga := range g.gauges {
124 n, err := fmt.Fprintf(w, "%s %f %d\n", name, ga.g.Value(), now)
125 if err != nil {
126 return count, err
127 }
128 count += int64(n)
129 }
130
131 for name, h := range g.histograms {
132 for _, p := range []struct {
133 s string
134 f float64
135 }{
136 {"50", 0.50},
137 {"90", 0.90},
138 {"95", 0.95},
139 {"99", 0.99},
140 } {
141 n, err := fmt.Fprintf(w, "%s.p%s %f %d\n", name, p.s, h.h.Quantile(p.f), now)
142 if err != nil {
143 return count, err
144 }
145 count += int64(n)
146 }
147 }
148
149 return count, err
150 }
151
152 // Counter is a Graphite counter metric.
153 type Counter struct {
154 c *generic.Counter
155 }
156
157 // NewCounter returns a new usable counter metric.
158 func NewCounter(name string) *Counter {
159 return &Counter{generic.NewCounter(name)}
160 }
161
162 // With is a no-op.
163 func (c *Counter) With(...string) metrics.Counter { return c }
164
165 // Add implements counter.
166 func (c *Counter) Add(delta float64) { c.c.Add(delta) }
167
168 // Gauge is a Graphite gauge metric.
169 type Gauge struct {
170 g *generic.Gauge
171 }
172
173 // NewGauge returns a new usable Gauge metric.
174 func NewGauge(name string) *Gauge {
175 return &Gauge{generic.NewGauge(name)}
176 }
177
178 // With is a no-op.
179 func (g *Gauge) With(...string) metrics.Gauge { return g }
180
181 // Set implements gauge.
182 func (g *Gauge) Set(value float64) { g.g.Set(value) }
183
184 // Histogram is a Graphite histogram metric. Observations are bucketed into
185 // per-quantile gauges.
186 type Histogram struct {
187 h *generic.Histogram
188 }
189
190 // NewHistogram returns a new usable Histogram metric.
191 func NewHistogram(name string, buckets int) *Histogram {
192 return &Histogram{generic.NewHistogram(name, buckets)}
193 }
194
195 // With is a no-op.
196 func (h *Histogram) With(...string) metrics.Histogram { return h }
197
198 // Observe implements histogram.
199 func (h *Histogram) Observe(value float64) { h.h.Observe(value) }
+0
-63
metrics3/graphite/graphite_test.go less more
0 package graphite
1
2 import (
3 "bytes"
4 "regexp"
5 "strconv"
6 "testing"
7
8 "github.com/go-kit/kit/log"
9 "github.com/go-kit/kit/metrics3/teststat"
10 )
11
12 func TestCounter(t *testing.T) {
13 prefix, name := "abc.", "def"
14 label, value := "label", "value" // ignored for Graphite
15 regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
16 g := New(prefix, log.NewNopLogger())
17 counter := g.NewCounter(name).With(label, value)
18 valuef := teststat.SumLines(g, regex)
19 if err := teststat.TestCounter(counter, valuef); err != nil {
20 t.Fatal(err)
21 }
22 }
23
24 func TestGauge(t *testing.T) {
25 prefix, name := "ghi.", "jkl"
26 label, value := "xyz", "abc" // ignored for Graphite
27 regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
28 g := New(prefix, log.NewNopLogger())
29 gauge := g.NewGauge(name).With(label, value)
30 valuef := teststat.LastLine(g, regex)
31 if err := teststat.TestGauge(gauge, valuef); err != nil {
32 t.Fatal(err)
33 }
34 }
35
36 func TestHistogram(t *testing.T) {
37 // The histogram test is actually like 4 gauge tests.
38 prefix, name := "statsd.", "histogram_test"
39 label, value := "abc", "def" // ignored for Graphite
40 re50 := regexp.MustCompile(prefix + name + `.p50 ([0-9\.]+) [0-9]+`)
41 re90 := regexp.MustCompile(prefix + name + `.p90 ([0-9\.]+) [0-9]+`)
42 re95 := regexp.MustCompile(prefix + name + `.p95 ([0-9\.]+) [0-9]+`)
43 re99 := regexp.MustCompile(prefix + name + `.p99 ([0-9\.]+) [0-9]+`)
44 g := New(prefix, log.NewNopLogger())
45 histogram := g.NewHistogram(name, 50).With(label, value)
46 quantiles := func() (float64, float64, float64, float64) {
47 var buf bytes.Buffer
48 g.WriteTo(&buf)
49 match50 := re50.FindStringSubmatch(buf.String())
50 p50, _ := strconv.ParseFloat(match50[1], 64)
51 match90 := re90.FindStringSubmatch(buf.String())
52 p90, _ := strconv.ParseFloat(match90[1], 64)
53 match95 := re95.FindStringSubmatch(buf.String())
54 p95, _ := strconv.ParseFloat(match95[1], 64)
55 match99 := re99.FindStringSubmatch(buf.String())
56 p99, _ := strconv.ParseFloat(match99[1], 64)
57 return p50, p90, p95, p99
58 }
59 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
60 t.Fatal(err)
61 }
62 }
+0
-249
metrics3/influx/influx.go less more
0 // Package influx provides an InfluxDB implementation for metrics. The model is
1 // similar to other push-based instrumentation systems. Observations are
2 // aggregated locally and emitted to the Influx server on regular intervals.
3 package influx
4
5 import (
6 "time"
7
8 influxdb "github.com/influxdata/influxdb/client/v2"
9
10 "github.com/go-kit/kit/log"
11 "github.com/go-kit/kit/metrics3"
12 "github.com/go-kit/kit/metrics3/internal/lv"
13 )
14
15 // Influx is a store for metrics that will be emitted to an Influx database.
16 //
17 // Influx is a general purpose time-series database, and has no native concepts
18 // of counters, gauges, or histograms. Counters are modeled as a timeseries with
19 // one data point per flush, with a "count" field that reflects all adds since
20 // the last flush. Gauges are modeled as a timeseries with one data point per
21 // flush, with a "value" field that reflects the current state of the gauge.
22 // Histograms are modeled as a timeseries with one data point per observation,
23 // with a "value" field that reflects each observation; use e.g. the HISTOGRAM
24 // aggregate function to compute histograms.
25 //
26 // Influx tags are immutable, attached to the Influx object, and given to each
27 // metric at construction. Influx fields are mapped to Go kit label values, and
28 // may be mutated via With functions. Actual metric values are provided as
29 // fields with specific names depending on the metric.
30 //
31 // All observations are collected in memory locally, and flushed on demand.
32 type Influx struct {
33 counters *lv.Space
34 gauges *lv.Space
35 histograms *lv.Space
36 tags map[string]string
37 conf influxdb.BatchPointsConfig
38 logger log.Logger
39 }
40
41 // New returns an Influx, ready to create metrics and collect observations. Tags
42 // are applied to all metrics created from this object. The BatchPointsConfig is
43 // used during flushing.
44 func New(tags map[string]string, conf influxdb.BatchPointsConfig, logger log.Logger) *Influx {
45 return &Influx{
46 counters: lv.NewSpace(),
47 gauges: lv.NewSpace(),
48 histograms: lv.NewSpace(),
49 tags: tags,
50 conf: conf,
51 logger: logger,
52 }
53 }
54
55 // NewCounter returns an Influx counter.
56 func (in *Influx) NewCounter(name string) *Counter {
57 return &Counter{
58 name: name,
59 obs: in.counters.Observe,
60 }
61 }
62
63 // NewGauge returns an Influx gauge.
64 func (in *Influx) NewGauge(name string) *Gauge {
65 return &Gauge{
66 name: name,
67 obs: in.gauges.Observe,
68 }
69 }
70
71 // NewHistogram returns an Influx histogram.
72 func (in *Influx) NewHistogram(name string) *Histogram {
73 return &Histogram{
74 name: name,
75 obs: in.histograms.Observe,
76 }
77 }
78
79 // BatchPointsWriter captures a subset of the influxdb.Client methods necessary
80 // for emitting metrics observations.
81 type BatchPointsWriter interface {
82 Write(influxdb.BatchPoints) error
83 }
84
85 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
86 // time the passed channel fires. This method blocks until the channel is
87 // closed, so clients probably want to run it in its own goroutine. For typical
88 // usage, create a time.Ticker and pass its C channel to this method.
89 func (in *Influx) WriteLoop(c <-chan time.Time, w BatchPointsWriter) {
90 for range c {
91 if err := in.WriteTo(w); err != nil {
92 in.logger.Log("during", "WriteTo", "err", err)
93 }
94 }
95 }
96
97 // WriteTo flushes the buffered content of the metrics to the writer, in an
98 // Influx BatchPoints format. WriteTo abides best-effort semantics, so
99 // observations are lost if there is a problem with the write. Clients should be
100 // sure to call WriteTo regularly, ideally through the WriteLoop helper method.
101 func (in *Influx) WriteTo(w BatchPointsWriter) (err error) {
102 bp, err := influxdb.NewBatchPoints(in.conf)
103 if err != nil {
104 return err
105 }
106
107 now := time.Now()
108
109 in.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
110 fields := fieldsFrom(lvs)
111 fields["count"] = sum(values)
112 var p *influxdb.Point
113 p, err = influxdb.NewPoint(name, in.tags, fields, now)
114 if err != nil {
115 return false
116 }
117 bp.AddPoint(p)
118 return true
119 })
120 if err != nil {
121 return err
122 }
123
124 in.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
125 fields := fieldsFrom(lvs)
126 fields["value"] = last(values)
127 var p *influxdb.Point
128 p, err = influxdb.NewPoint(name, in.tags, fields, now)
129 if err != nil {
130 return false
131 }
132 bp.AddPoint(p)
133 return true
134 })
135 if err != nil {
136 return err
137 }
138
139 in.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
140 fields := fieldsFrom(lvs)
141 ps := make([]*influxdb.Point, len(values))
142 for i, v := range values {
143 fields["value"] = v // overwrite each time
144 ps[i], err = influxdb.NewPoint(name, in.tags, fields, now)
145 if err != nil {
146 return false
147 }
148 }
149 bp.AddPoints(ps)
150 return true
151 })
152 if err != nil {
153 return err
154 }
155
156 return w.Write(bp)
157 }
158
159 func fieldsFrom(labelValues []string) map[string]interface{} {
160 if len(labelValues)%2 != 0 {
161 panic("fieldsFrom received a labelValues with an odd number of strings")
162 }
163 fields := make(map[string]interface{}, len(labelValues)/2)
164 for i := 0; i < len(labelValues); i += 2 {
165 fields[labelValues[i]] = labelValues[i+1]
166 }
167 return fields
168 }
169
170 func sum(a []float64) float64 {
171 var v float64
172 for _, f := range a {
173 v += f
174 }
175 return v
176 }
177
178 func last(a []float64) float64 {
179 return a[len(a)-1]
180 }
181
182 type observeFunc func(name string, lvs lv.LabelValues, value float64)
183
184 // Counter is an Influx counter. Observations are forwarded to an Influx
185 // object, and aggregated (summed) per timeseries.
186 type Counter struct {
187 name string
188 lvs lv.LabelValues
189 obs observeFunc
190 }
191
192 // With implements metrics.Counter.
193 func (c *Counter) With(labelValues ...string) metrics.Counter {
194 return &Counter{
195 name: c.name,
196 lvs: c.lvs.With(labelValues...),
197 obs: c.obs,
198 }
199 }
200
201 // Add implements metrics.Counter.
202 func (c *Counter) Add(delta float64) {
203 c.obs(c.name, c.lvs, delta)
204 }
205
206 // Gauge is an Influx gauge. Observations are forwarded to a Dogstatsd
207 // object, and aggregated (the last observation selected) per timeseries.
208 type Gauge struct {
209 name string
210 lvs lv.LabelValues
211 obs observeFunc
212 }
213
214 // With implements metrics.Gauge.
215 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
216 return &Gauge{
217 name: g.name,
218 lvs: g.lvs.With(labelValues...),
219 obs: g.obs,
220 }
221 }
222
223 // Set implements metrics.Gauge.
224 func (g *Gauge) Set(value float64) {
225 g.obs(g.name, g.lvs, value)
226 }
227
228 // Histogram is an Influx histrogram. Observations are aggregated into a
229 // generic.Histogram and emitted as per-quantile gauges to the Influx server.
230 type Histogram struct {
231 name string
232 lvs lv.LabelValues
233 obs observeFunc
234 }
235
236 // With implements metrics.Histogram.
237 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
238 return &Histogram{
239 name: h.name,
240 lvs: h.lvs.With(labelValues...),
241 obs: h.obs,
242 }
243 }
244
245 // Observe implements metrics.Histogram.
246 func (h *Histogram) Observe(value float64) {
247 h.obs(h.name, h.lvs, value)
248 }
+0
-92
metrics3/influx/influx_test.go less more
0 package influx
1
2 import (
3 "bytes"
4 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8 "testing"
9
10 "github.com/go-kit/kit/log"
11 "github.com/go-kit/kit/metrics3/generic"
12 "github.com/go-kit/kit/metrics3/teststat"
13 influxdb "github.com/influxdata/influxdb/client/v2"
14 )
15
16 func TestCounter(t *testing.T) {
17 in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
18 re := regexp.MustCompile(`influx_counter,a=b count=([0-9\.]+) [0-9]+`) // reverse-engineered :\
19 counter := in.NewCounter("influx_counter")
20 value := func() float64 {
21 client := &bufWriter{}
22 in.WriteTo(client)
23 match := re.FindStringSubmatch(client.buf.String())
24 f, _ := strconv.ParseFloat(match[1], 64)
25 return f
26 }
27 if err := teststat.TestCounter(counter, value); err != nil {
28 t.Fatal(err)
29 }
30 }
31
32 func TestGauge(t *testing.T) {
33 in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
34 re := regexp.MustCompile(`influx_gauge,foo=alpha value=([0-9\.]+) [0-9]+`)
35 gauge := in.NewGauge("influx_gauge")
36 value := func() float64 {
37 client := &bufWriter{}
38 in.WriteTo(client)
39 match := re.FindStringSubmatch(client.buf.String())
40 f, _ := strconv.ParseFloat(match[1], 64)
41 return f
42 }
43 if err := teststat.TestGauge(gauge, value); err != nil {
44 t.Fatal(err)
45 }
46 }
47
48 func TestHistogram(t *testing.T) {
49 in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
50 re := regexp.MustCompile(`influx_histogram,foo=alpha bar="beta",value=([0-9\.]+) [0-9]+`)
51 histogram := in.NewHistogram("influx_histogram").With("bar", "beta")
52 quantiles := func() (float64, float64, float64, float64) {
53 w := &bufWriter{}
54 in.WriteTo(w)
55 h := generic.NewHistogram("h", 50)
56 matches := re.FindAllStringSubmatch(w.buf.String(), -1)
57 for _, match := range matches {
58 f, _ := strconv.ParseFloat(match[1], 64)
59 h.Observe(f)
60 }
61 return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99)
62 }
63 if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
64 t.Fatal(err)
65 }
66 }
67
68 func TestHistogramLabels(t *testing.T) {
69 in := New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
70 h := in.NewHistogram("foo")
71 h.Observe(123)
72 h.With("abc", "xyz").Observe(456)
73 w := &bufWriter{}
74 if err := in.WriteTo(w); err != nil {
75 t.Fatal(err)
76 }
77 if want, have := 2, len(strings.Split(strings.TrimSpace(w.buf.String()), "\n")); want != have {
78 t.Errorf("want %d, have %d", want, have)
79 }
80 }
81
82 type bufWriter struct {
83 buf bytes.Buffer
84 }
85
86 func (w *bufWriter) Write(bp influxdb.BatchPoints) error {
87 for _, p := range bp.Points() {
88 fmt.Fprintf(&w.buf, p.String()+"\n")
89 }
90 return nil
91 }
+0
-94
metrics3/internal/emitting/buffer.go less more
0 package emitting
1
2 import (
3 "fmt"
4 "strings"
5 "sync"
6
7 "sort"
8
9 "github.com/go-kit/kit/metrics3/generic"
10 )
11
12 type Buffer struct {
13 buckets int
14
15 mtx sync.Mutex
16 counters map[point]*generic.Counter
17 gauges map[point]*generic.Gauge
18 histograms map[point]*generic.Histogram
19 }
20
21 func (b *Buffer) Add(a Add) {
22 pt := makePoint(a.Name, a.LabelValues)
23 b.mtx.Lock()
24 defer b.mtx.Unlock()
25 c, ok := b.counters[pt]
26 if !ok {
27 c = generic.NewCounter(a.Name).With(a.LabelValues...).(*generic.Counter)
28 }
29 c.Add(a.Delta)
30 b.counters[pt] = c
31 }
32
33 func (b *Buffer) Set(s Set) {
34 pt := makePoint(s.Name, s.LabelValues)
35 b.mtx.Lock()
36 defer b.mtx.Unlock()
37 g, ok := b.gauges[pt]
38 if !ok {
39 g = generic.NewGauge(s.Name).With(s.LabelValues...).(*generic.Gauge)
40 }
41 g.Set(s.Value)
42 b.gauges[pt] = g
43 }
44
45 func (b *Buffer) Obv(o Obv) {
46 pt := makePoint(o.Name, o.LabelValues)
47 b.mtx.Lock()
48 defer b.mtx.Unlock()
49 h, ok := b.histograms[pt]
50 if !ok {
51 h = generic.NewHistogram(o.Name, b.buckets).With(o.LabelValues...).(*generic.Histogram)
52 }
53 h.Observe(o.Value)
54 b.histograms[pt] = h
55 }
56
57 // point as in point in N-dimensional vector space;
58 // a string encoding of name + sorted k/v pairs.
59 type point string
60
61 const (
62 recordDelimiter = "•"
63 fieldDelimiter = "·"
64 )
65
66 // (foo, [a b c d]) => "foo•a·b•c·d"
67 func makePoint(name string, labelValues []string) point {
68 if len(labelValues)%2 != 0 {
69 panic("odd number of label values; programmer error!")
70 }
71 pairs := make([]string, 0, len(labelValues)/2)
72 for i := 0; i < len(labelValues); i += 2 {
73 pairs = append(pairs, fmt.Sprintf("%s%s%s", labelValues[i], fieldDelimiter, labelValues[i+1]))
74 }
75 sort.Strings(sort.StringSlice(pairs))
76 pairs = append([]string{name}, pairs...)
77 return point(strings.Join(pairs, recordDelimiter))
78 }
79
80 // "foo•a·b•c·d" => (foo, [a b c d])
81 func (p point) nameLabelValues() (name string, labelValues []string) {
82 records := strings.Split(string(p), recordDelimiter)
83 if len(records)%2 != 1 { // always name + even number of label/values
84 panic("even number of point records; programmer error!")
85 }
86 name, records = records[0], records[1:]
87 labelValues = make([]string, 0, len(records)*2)
88 for _, record := range records {
89 fields := strings.SplitN(record, fieldDelimiter, 2)
90 labelValues = append(labelValues, fields[0], fields[1])
91 }
92 return name, labelValues
93 }
+0
-107
metrics3/internal/emitting/metrics.go less more
0 package emitting
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/internal/lv"
5 )
6
7 type Counter struct {
8 name string
9 lvs lv.LabelValues
10 sampleRate float64
11 c chan Add
12 }
13
14 type Add struct {
15 Name string
16 LabelValues []string
17 SampleRate float64
18 Delta float64
19 }
20
21 func NewCounter(name string, sampleRate float64, c chan Add) *Counter {
22 return &Counter{
23 name: name,
24 sampleRate: sampleRate,
25 c: c,
26 }
27 }
28
29 func (c *Counter) With(labelValues ...string) metrics.Counter {
30 return &Counter{
31 name: c.name,
32 lvs: c.lvs.With(labelValues...),
33 sampleRate: c.sampleRate,
34 c: c.c,
35 }
36 }
37
38 func (c *Counter) Add(delta float64) {
39 c.c <- Add{c.name, c.lvs, c.sampleRate, delta}
40 }
41
42 type Gauge struct {
43 name string
44 lvs lv.LabelValues
45 c chan Set
46 }
47
48 type Set struct {
49 Name string
50 LabelValues []string
51 Value float64
52 }
53
54 func NewGauge(name string, c chan Set) *Gauge {
55 return &Gauge{
56 name: name,
57 c: c,
58 }
59 }
60
61 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
62 return &Gauge{
63 name: g.name,
64 lvs: g.lvs.With(labelValues...),
65 c: g.c,
66 }
67 }
68
69 func (g *Gauge) Set(value float64) {
70 g.c <- Set{g.name, g.lvs, value}
71 }
72
73 type Histogram struct {
74 name string
75 lvs lv.LabelValues
76 sampleRate float64
77 c chan Obv
78 }
79
80 type Obv struct {
81 Name string
82 LabelValues []string
83 SampleRate float64
84 Value float64
85 }
86
87 func NewHistogram(name string, sampleRate float64, c chan Obv) *Histogram {
88 return &Histogram{
89 name: name,
90 sampleRate: sampleRate,
91 c: c,
92 }
93 }
94
95 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
96 return &Histogram{
97 name: h.name,
98 lvs: h.lvs.With(labelValues...),
99 sampleRate: h.sampleRate,
100 c: h.c,
101 }
102 }
103
104 func (h *Histogram) Observe(value float64) {
105 h.c <- Obv{h.name, h.lvs, h.sampleRate, value}
106 }
+0
-14
metrics3/internal/lv/labelvalues.go less more
0 package lv
1
2 // LabelValues is a type alias that provides validation on its With method.
3 // Metrics may include it as a member to help them satisfy With semantics and
4 // save some code duplication.
5 type LabelValues []string
6
7 // With validates the input, and returns a new aggregate labelValues.
8 func (lvs LabelValues) With(labelValues ...string) LabelValues {
9 if len(labelValues)%2 != 0 {
10 labelValues = append(labelValues, "unknown")
11 }
12 return append(lvs, labelValues...)
13 }
+0
-22
metrics3/internal/lv/labelvalues_test.go less more
0 package lv
1
2 import (
3 "strings"
4 "testing"
5 )
6
7 func TestWith(t *testing.T) {
8 var a LabelValues
9 b := a.With("a", "1")
10 c := a.With("b", "2", "c", "3")
11
12 if want, have := "", strings.Join(a, ""); want != have {
13 t.Errorf("With appears to mutate the original LabelValues: want %q, have %q", want, have)
14 }
15 if want, have := "a1", strings.Join(b, ""); want != have {
16 t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
17 }
18 if want, have := "b2c3", strings.Join(c, ""); want != have {
19 t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
20 }
21 }
+0
-106
metrics3/internal/lv/space.go less more
0 package lv
1
2 import "sync"
3
4 // NewSpace returns an N-dimensional vector space.
5 func NewSpace() *Space {
6 return &Space{}
7 }
8
9 // Space represents an N-dimensional vector space. Each name and unique label
10 // value pair establishes a new dimension and point within that dimension. Order
11 // matters, i.e. [a=1 b=2] identifies a different timeseries than [b=2 a=1].
12 type Space struct {
13 mtx sync.RWMutex
14 nodes map[string]*node
15 }
16
17 // Observe locates the time series identified by the name and label values in
18 // the vector space, and appends the value to the list of observations.
19 func (s *Space) Observe(name string, lvs LabelValues, value float64) {
20 s.nodeFor(name).observe(lvs, value)
21 }
22
23 // Walk traverses the vector space and invokes fn for each non-empty time series
24 // which is encountered. Return false to abort the traversal.
25 func (s *Space) Walk(fn func(name string, lvs LabelValues, observations []float64) bool) {
26 s.mtx.RLock()
27 defer s.mtx.RUnlock()
28 for name, node := range s.nodes {
29 f := func(lvs LabelValues, observations []float64) bool { return fn(name, lvs, observations) }
30 if !node.walk(LabelValues{}, f) {
31 return
32 }
33 }
34 }
35
36 // Reset empties the current space and returns a new Space with the old
37 // contents. Reset a Space to get an immutable copy suitable for walking.
38 func (s *Space) Reset() *Space {
39 s.mtx.Lock()
40 defer s.mtx.Unlock()
41 n := NewSpace()
42 n.nodes, s.nodes = s.nodes, n.nodes
43 return n
44 }
45
46 func (s *Space) nodeFor(name string) *node {
47 s.mtx.Lock()
48 defer s.mtx.Unlock()
49 if s.nodes == nil {
50 s.nodes = map[string]*node{}
51 }
52 n, ok := s.nodes[name]
53 if !ok {
54 n = &node{}
55 s.nodes[name] = n
56 }
57 return n
58 }
59
60 // node exists at a specific point in the N-dimensional vector space of all
61 // possible label values. The node collects observations and has child nodes
62 // with greater specificity.
63 type node struct {
64 mtx sync.RWMutex
65 observations []float64
66 children map[pair]*node
67 }
68
69 type pair struct{ label, value string }
70
71 func (n *node) observe(lvs LabelValues, value float64) {
72 n.mtx.Lock()
73 defer n.mtx.Unlock()
74 if len(lvs) == 0 {
75 n.observations = append(n.observations, value)
76 return
77 }
78 if len(lvs) < 2 {
79 panic("too few LabelValues; programmer error!")
80 }
81 head, tail := pair{lvs[0], lvs[1]}, lvs[2:]
82 if n.children == nil {
83 n.children = map[pair]*node{}
84 }
85 child, ok := n.children[head]
86 if !ok {
87 child = &node{}
88 n.children[head] = child
89 }
90 child.observe(tail, value)
91 }
92
93 func (n *node) walk(lvs LabelValues, fn func(LabelValues, []float64) bool) bool {
94 n.mtx.RLock()
95 defer n.mtx.RUnlock()
96 if len(n.observations) > 0 && !fn(lvs, n.observations) {
97 return false
98 }
99 for p, child := range n.children {
100 if !child.walk(append(lvs, p.label, p.value), fn) {
101 return false
102 }
103 }
104 return true
105 }
+0
-86
metrics3/internal/lv/space_test.go less more
0 package lv
1
2 import (
3 "strings"
4 "testing"
5 )
6
7 func TestSpaceWalkAbort(t *testing.T) {
8 s := NewSpace()
9 s.Observe("a", LabelValues{"a", "b"}, 1)
10 s.Observe("a", LabelValues{"c", "d"}, 2)
11 s.Observe("a", LabelValues{"e", "f"}, 4)
12 s.Observe("a", LabelValues{"g", "h"}, 8)
13 s.Observe("b", LabelValues{"a", "b"}, 16)
14 s.Observe("b", LabelValues{"c", "d"}, 32)
15 s.Observe("b", LabelValues{"e", "f"}, 64)
16 s.Observe("b", LabelValues{"g", "h"}, 128)
17
18 var count int
19 s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
20 count++
21 return false
22 })
23 if want, have := 1, count; want != have {
24 t.Errorf("want %d, have %d", want, have)
25 }
26 }
27
28 func TestSpaceWalkSums(t *testing.T) {
29 s := NewSpace()
30 s.Observe("metric_one", LabelValues{}, 1)
31 s.Observe("metric_one", LabelValues{}, 2)
32 s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 4)
33 s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 8)
34 s.Observe("metric_one", LabelValues{}, 16)
35 s.Observe("metric_one", LabelValues{"a", "1", "b", "3"}, 32)
36 s.Observe("metric_two", LabelValues{}, 64)
37 s.Observe("metric_two", LabelValues{}, 128)
38 s.Observe("metric_two", LabelValues{"a", "1", "b", "2"}, 256)
39
40 have := map[string]float64{}
41 s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
42 //t.Logf("%s %v => %v", name, lvs, obs)
43 have[name+" ["+strings.Join(lvs, "")+"]"] += sum(obs)
44 return true
45 })
46
47 want := map[string]float64{
48 "metric_one []": 1 + 2 + 16,
49 "metric_one [a1b2]": 4 + 8,
50 "metric_one [a1b3]": 32,
51 "metric_two []": 64 + 128,
52 "metric_two [a1b2]": 256,
53 }
54 for keystr, wantsum := range want {
55 if havesum := have[keystr]; wantsum != havesum {
56 t.Errorf("%q: want %.1f, have %.1f", keystr, wantsum, havesum)
57 }
58 delete(want, keystr)
59 delete(have, keystr)
60 }
61 for keystr, havesum := range have {
62 t.Errorf("%q: unexpected observations recorded: %.1f", keystr, havesum)
63 }
64 }
65
66 func TestSpaceWalkSkipsEmptyDimensions(t *testing.T) {
67 s := NewSpace()
68 s.Observe("foo", LabelValues{"bar", "1", "baz", "2"}, 123)
69
70 var count int
71 s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
72 count++
73 return true
74 })
75 if want, have := 1, count; want != have {
76 t.Errorf("want %d, have %d", want, have)
77 }
78 }
79
80 func sum(a []float64) (v float64) {
81 for _, f := range a {
82 v += f
83 }
84 return
85 }
+0
-40
metrics3/internal/ratemap/ratemap.go less more
0 // Package ratemap implements a goroutine-safe map of string to float64. It can
1 // be embedded in implementations whose metrics support fixed sample rates, so
2 // that an additional parameter doesn't have to be tracked through the e.g.
3 // lv.Space object.
4 package ratemap
5
6 import "sync"
7
8 // RateMap is a simple goroutine-safe map of string to float64.
9 type RateMap struct {
10 mtx sync.RWMutex
11 m map[string]float64
12 }
13
14 // New returns a new RateMap.
15 func New() *RateMap {
16 return &RateMap{
17 m: map[string]float64{},
18 }
19 }
20
21 // Set writes the given name/rate pair to the map.
22 // Set is safe for concurrent access by multiple goroutines.
23 func (m *RateMap) Set(name string, rate float64) {
24 m.mtx.Lock()
25 defer m.mtx.Unlock()
26 m.m[name] = rate
27 }
28
29 // Get retrieves the rate for the given name, or 1.0 if none is set.
30 // Get is safe for concurrent access by multiple goroutines.
31 func (m *RateMap) Get(name string) float64 {
32 m.mtx.RLock()
33 defer m.mtx.RUnlock()
34 f, ok := m.m[name]
35 if !ok {
36 f = 1.0
37 }
38 return f
39 }
+0
-24
metrics3/metrics.go less more
0 package metrics
1
2 // Counter describes a metric that accumulates values monotonically.
3 // An example of a counter is the number of received HTTP requests.
4 type Counter interface {
5 With(labelValues ...string) Counter
6 Add(delta float64)
7 }
8
9 // Gauge describes a metric that takes specific values over time.
10 // An example of a gauge is the current depth of a job queue.
11 type Gauge interface {
12 With(labelValues ...string) Gauge
13 Set(value float64)
14 }
15
16 // Histogram describes a metric that takes repeated observations of the same
17 // kind of thing, and produces a statistical summary of those observations,
18 // typically expressed as quantiles or buckets. An example of a histogram is HTTP
19 // request latencies.
20 type Histogram interface {
21 With(labelValues ...string) Histogram
22 Observe(value float64)
23 }
+0
-157
metrics3/prometheus/prometheus.go less more
0 // Package prometheus provides Prometheus implementations for metrics.
1 // Individual metrics are mapped to their Prometheus counterparts, and
2 // (depending on the constructor used) may be automatically registered in the
3 // global Prometheus metrics registry.
4 package prometheus
5
6 import (
7 "github.com/prometheus/client_golang/prometheus"
8
9 "github.com/go-kit/kit/metrics3"
10 "github.com/go-kit/kit/metrics3/internal/lv"
11 )
12
13 // Counter implements Counter, via a Prometheus CounterVec.
14 type Counter struct {
15 cv *prometheus.CounterVec
16 lvs lv.LabelValues
17 }
18
19 // NewCounterFrom constructs and registers a Prometheus CounterVec,
20 // and returns a usable Counter object.
21 func NewCounterFrom(opts prometheus.CounterOpts, labelNames []string) *Counter {
22 cv := prometheus.NewCounterVec(opts, labelNames)
23 prometheus.MustRegister(cv)
24 return NewCounter(cv)
25 }
26
27 // NewCounter wraps the CounterVec and returns a usable Counter object.
28 func NewCounter(cv *prometheus.CounterVec) *Counter {
29 return &Counter{
30 cv: cv,
31 }
32 }
33
34 // With implements Counter.
35 func (c *Counter) With(labelValues ...string) metrics.Counter {
36 return &Counter{
37 cv: c.cv,
38 lvs: c.lvs.With(labelValues...),
39 }
40 }
41
42 // Add implements Counter.
43 func (c *Counter) Add(delta float64) {
44 c.cv.WithLabelValues(c.lvs...).Add(delta)
45 }
46
47 // Gauge implements Gauge, via a Prometheus GaugeVec.
48 type Gauge struct {
49 gv *prometheus.GaugeVec
50 lvs lv.LabelValues
51 }
52
53 // NewGaugeFrom construts and registers a Prometheus GaugeVec,
54 // and returns a usable Gauge object.
55 func NewGaugeFrom(opts prometheus.GaugeOpts, labelNames []string) *Gauge {
56 gv := prometheus.NewGaugeVec(opts, labelNames)
57 prometheus.MustRegister(gv)
58 return NewGauge(gv)
59 }
60
61 // NewGauge wraps the GaugeVec and returns a usable Gauge object.
62 func NewGauge(gv *prometheus.GaugeVec) *Gauge {
63 return &Gauge{
64 gv: gv,
65 }
66 }
67
68 // With implements Gauge.
69 func (g *Gauge) With(labelValues ...string) metrics.Gauge {
70 return &Gauge{
71 gv: g.gv,
72 lvs: g.lvs.With(labelValues...),
73 }
74 }
75
76 // Set implements Gauge.
77 func (g *Gauge) Set(value float64) {
78 g.gv.WithLabelValues(g.lvs...).Set(value)
79 }
80
81 // Add is supported by Prometheus GaugeVecs.
82 func (g *Gauge) Add(delta float64) {
83 g.gv.WithLabelValues(g.lvs...).Add(delta)
84 }
85
86 // Summary implements Histogram, via a Prometheus SummaryVec. The difference
87 // between a Summary and a Histogram is that Summaries don't require predefined
88 // quantile buckets, but cannot be statistically aggregated.
89 type Summary struct {
90 sv *prometheus.SummaryVec
91 lvs lv.LabelValues
92 }
93
94 // NewSummaryFrom constructs and registers a Prometheus SummaryVec,
95 // and returns a usable Summary object.
96 func NewSummaryFrom(opts prometheus.SummaryOpts, labelNames []string) *Summary {
97 sv := prometheus.NewSummaryVec(opts, labelNames)
98 prometheus.MustRegister(sv)
99 return NewSummary(sv)
100 }
101
102 // NewSummary wraps the SummaryVec and returns a usable Summary object.
103 func NewSummary(sv *prometheus.SummaryVec) *Summary {
104 return &Summary{
105 sv: sv,
106 }
107 }
108
109 // With implements Histogram.
110 func (s *Summary) With(labelValues ...string) metrics.Histogram {
111 return &Summary{
112 sv: s.sv,
113 lvs: s.lvs.With(labelValues...),
114 }
115 }
116
117 // Observe implements Histogram.
118 func (s *Summary) Observe(value float64) {
119 s.sv.WithLabelValues(s.lvs...).Observe(value)
120 }
121
122 // Histogram implements Histogram via a Prometheus HistogramVec. The difference
123 // between a Histogram and a Summary is that Histograms require predefined
124 // quantile buckets, and can be statistically aggregated.
125 type Histogram struct {
126 hv *prometheus.HistogramVec
127 lvs lv.LabelValues
128 }
129
130 // NewHistogramFrom constructs and registers a Prometheus HistogramVec,
131 // and returns a usable Histogram object.
132 func NewHistogramFrom(opts prometheus.HistogramOpts, labelNames []string) *Histogram {
133 hv := prometheus.NewHistogramVec(opts, labelNames)
134 prometheus.MustRegister(hv)
135 return NewHistogram(hv)
136 }
137
138 // NewHistogram wraps the HistogramVec and returns a usable Histogram object.
139 func NewHistogram(hv *prometheus.HistogramVec) *Histogram {
140 return &Histogram{
141 hv: hv,
142 }
143 }
144
145 // With implements Histogram.
146 func (h *Histogram) With(labelValues ...string) metrics.Histogram {
147 return &Histogram{
148 hv: h.hv,
149 lvs: h.lvs.With(labelValues...),
150 }
151 }
152
153 // Observe implements Histogram.
154 func (h *Histogram) Observe(value float64) {
155 h.hv.WithLabelValues(h.lvs...).Observe(value)
156 }
+0
-191
metrics3/prometheus/prometheus_test.go less more
0 package prometheus
1
2 import (
3 "io/ioutil"
4 "math"
5 "math/rand"
6 "net/http"
7 "net/http/httptest"
8 "regexp"
9 "strconv"
10 "strings"
11 "testing"
12
13 "github.com/go-kit/kit/metrics3/teststat"
14 stdprometheus "github.com/prometheus/client_golang/prometheus"
15 )
16
17 func TestCounter(t *testing.T) {
18 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
19 defer s.Close()
20
21 scrape := func() string {
22 resp, _ := http.Get(s.URL)
23 buf, _ := ioutil.ReadAll(resp.Body)
24 return string(buf)
25 }
26
27 namespace, subsystem, name := "ns", "ss", "foo"
28 re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + ` ([0-9\.]+)`)
29
30 counter := NewCounterFrom(stdprometheus.CounterOpts{
31 Namespace: namespace,
32 Subsystem: subsystem,
33 Name: name,
34 Help: "This is the help string.",
35 }, []string{})
36
37 value := func() float64 {
38 matches := re.FindStringSubmatch(scrape())
39 f, _ := strconv.ParseFloat(matches[1], 64)
40 return f
41 }
42
43 if err := teststat.TestCounter(counter, value); err != nil {
44 t.Fatal(err)
45 }
46 }
47
48 func TestGauge(t *testing.T) {
49 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
50 defer s.Close()
51
52 scrape := func() string {
53 resp, _ := http.Get(s.URL)
54 buf, _ := ioutil.ReadAll(resp.Body)
55 return string(buf)
56 }
57
58 namespace, subsystem, name := "aaa", "bbb", "ccc"
59 re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + ` ([0-9\.]+)`)
60
61 gauge := NewGaugeFrom(stdprometheus.GaugeOpts{
62 Namespace: namespace,
63 Subsystem: subsystem,
64 Name: name,
65 Help: "This is a different help string.",
66 }, []string{})
67
68 value := func() float64 {
69 matches := re.FindStringSubmatch(scrape())
70 f, _ := strconv.ParseFloat(matches[1], 64)
71 return f
72 }
73
74 if err := teststat.TestGauge(gauge, value); err != nil {
75 t.Fatal(err)
76 }
77 }
78
79 func TestSummary(t *testing.T) {
80 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
81 defer s.Close()
82
83 scrape := func() string {
84 resp, _ := http.Get(s.URL)
85 buf, _ := ioutil.ReadAll(resp.Body)
86 return string(buf)
87 }
88
89 namespace, subsystem, name := "test", "prometheus", "summary"
90 re50 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{quantile="0.5"} ([0-9\.]+)`)
91 re90 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{quantile="0.9"} ([0-9\.]+)`)
92 re99 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{quantile="0.99"} ([0-9\.]+)`)
93
94 summary := NewSummaryFrom(stdprometheus.SummaryOpts{
95 Namespace: namespace,
96 Subsystem: subsystem,
97 Name: name,
98 Help: "This is the help string for the summary.",
99 }, []string{})
100
101 quantiles := func() (float64, float64, float64, float64) {
102 buf := scrape()
103 match50 := re50.FindStringSubmatch(buf)
104 p50, _ := strconv.ParseFloat(match50[1], 64)
105 match90 := re90.FindStringSubmatch(buf)
106 p90, _ := strconv.ParseFloat(match90[1], 64)
107 match99 := re99.FindStringSubmatch(buf)
108 p99, _ := strconv.ParseFloat(match99[1], 64)
109 p95 := p90 + ((p99 - p90) / 2) // Prometheus, y u no p95??? :< #yolo
110 return p50, p90, p95, p99
111 }
112
113 if err := teststat.TestHistogram(summary, quantiles, 0.01); err != nil {
114 t.Fatal(err)
115 }
116 }
117
118 func TestHistogram(t *testing.T) {
119 // Prometheus reports histograms as a count of observations that fell into
120 // each predefined bucket, with the bucket value representing a global upper
121 // limit. That is, the count monotonically increases over the buckets. This
122 // requires a different strategy to test.
123
124 s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
125 defer s.Close()
126
127 scrape := func() string {
128 resp, _ := http.Get(s.URL)
129 buf, _ := ioutil.ReadAll(resp.Body)
130 return string(buf)
131 }
132
133 namespace, subsystem, name := "test", "prometheus", "histogram"
134 re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `_bucket{le="([0-9]+|\+Inf)"} ([0-9\.]+)`)
135
136 numStdev := 3
137 bucketMin := (teststat.Mean - (numStdev * teststat.Stdev))
138 bucketMax := (teststat.Mean + (numStdev * teststat.Stdev))
139 if bucketMin < 0 {
140 bucketMin = 0
141 }
142 bucketCount := 10
143 bucketDelta := (bucketMax - bucketMin) / bucketCount
144 buckets := []float64{}
145 for i := bucketMin; i <= bucketMax; i += bucketDelta {
146 buckets = append(buckets, float64(i))
147 }
148
149 histogram := NewHistogramFrom(stdprometheus.HistogramOpts{
150 Namespace: namespace,
151 Subsystem: subsystem,
152 Name: name,
153 Help: "This is the help string for the histogram.",
154 Buckets: buckets,
155 }, []string{})
156
157 // Can't TestHistogram, because Prometheus Histograms don't dynamically
158 // compute quantiles. Instead, they fill up buckets. So, let's populate the
159 // histogram kind of manually.
160 teststat.PopulateNormalHistogram(histogram, rand.Int())
161
162 // Then, we use ExpectedObservationsLessThan to validate.
163 for _, line := range strings.Split(scrape(), "\n") {
164 match := re.FindStringSubmatch(line)
165 if match == nil {
166 continue
167 }
168
169 bucket, _ := strconv.ParseInt(match[1], 10, 64)
170 have, _ := strconv.ParseInt(match[2], 10, 64)
171
172 want := teststat.ExpectedObservationsLessThan(bucket)
173 if match[1] == "+Inf" {
174 want = int64(teststat.Count) // special case
175 }
176
177 // Unfortunately, we observe experimentally that Prometheus is quite
178 // imprecise at the extremes. I'm setting a very high tolerance for now.
179 // It would be great to dig in and figure out whether that's a problem
180 // with my Expected calculation, or in Prometheus.
181 tolerance := 0.25
182 if delta := math.Abs(float64(want) - float64(have)); (delta / float64(want)) > tolerance {
183 t.Errorf("Bucket %d: want %d, have %d (%.1f%%)", bucket, want, have, (100.0 * delta / float64(want)))
184 }
185 }
186 }
187
188 func TestWith(t *testing.T) {
189 t.Skip("TODO")
190 }
+0
-36
metrics3/provider/circonus.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/circonus"
5 )
6
7 type circonusProvider struct {
8 c *circonus.Circonus
9 }
10
11 // NewCirconusProvider takes the given Circonnus object and returns a Provider
12 // that produces Circonus metrics.
13 func NewCirconusProvider(c *circonus.Circonus) Provider {
14 return &circonusProvider{
15 c: c,
16 }
17 }
18
19 // NewCounter implements Provider.
20 func (p *circonusProvider) NewCounter(name string) metrics.Counter {
21 return p.c.NewCounter(name)
22 }
23
24 // NewGauge implements Provider.
25 func (p *circonusProvider) NewGauge(name string) metrics.Gauge {
26 return p.c.NewGauge(name)
27 }
28
29 // NewHistogram implements Provider. The buckets parameter is ignored.
30 func (p *circonusProvider) NewHistogram(name string, _ int) metrics.Histogram {
31 return p.c.NewHistogram(name)
32 }
33
34 // Stop implements Provider, but is a no-op.
35 func (p *circonusProvider) Stop() {}
+0
-24
metrics3/provider/discard.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/discard"
5 )
6
7 type discardProvider struct{}
8
9 // NewDiscardProvider returns a provider that produces no-op metrics via the
10 // discarding backend.
11 func NewDiscardProvider() Provider { return discardProvider{} }
12
13 // NewCounter implements Provider.
14 func (discardProvider) NewCounter(string) metrics.Counter { return discard.NewCounter() }
15
16 // NewGauge implements Provider.
17 func (discardProvider) NewGauge(string) metrics.Gauge { return discard.NewGauge() }
18
19 // NewHistogram implements Provider.
20 func (discardProvider) NewHistogram(string, int) metrics.Histogram { return discard.NewHistogram() }
21
22 // Stop implements Provider.
23 func (discardProvider) Stop() {}
+0
-43
metrics3/provider/dogstatsd.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/dogstatsd"
5 )
6
7 type dogstatsdProvider struct {
8 d *dogstatsd.Dogstatsd
9 stop func()
10 }
11
12 // NewDogstatsdProvider wraps the given Dogstatsd object and stop func and
13 // returns a Provider that produces Dogstatsd metrics. A typical stop function
14 // would be ticker.Stop from the ticker passed to the SendLoop helper method.
15 func NewDogstatsdProvider(d *dogstatsd.Dogstatsd, stop func()) Provider {
16 return &dogstatsdProvider{
17 d: d,
18 stop: stop,
19 }
20 }
21
22 // NewCounter implements Provider, returning a new Dogstatsd Counter with a
23 // sample rate of 1.0.
24 func (p *dogstatsdProvider) NewCounter(name string) metrics.Counter {
25 return p.d.NewCounter(name, 1.0)
26 }
27
28 // NewGauge implements Provider.
29 func (p *dogstatsdProvider) NewGauge(name string) metrics.Gauge {
30 return p.d.NewGauge(name)
31 }
32
33 // NewHistogram implements Provider, returning a new Dogstatsd Histogram (note:
34 // not a Timing) with a sample rate of 1.0. The buckets argument is ignored.
35 func (p *dogstatsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
36 return p.d.NewHistogram(name, 1.0)
37 }
38
39 // Stop implements Provider, invoking the stop function passed at construction.
40 func (p *dogstatsdProvider) Stop() {
41 p.stop()
42 }
+0
-31
metrics3/provider/expvar.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/expvar"
5 )
6
7 type expvarProvider struct{}
8
9 // NewExpvarProvider returns a Provider that produces expvar metrics.
10 func NewExpvarProvider() Provider {
11 return expvarProvider{}
12 }
13
14 // NewCounter implements Provider.
15 func (p expvarProvider) NewCounter(name string) metrics.Counter {
16 return expvar.NewCounter(name)
17 }
18
19 // NewGauge implements Provider.
20 func (p expvarProvider) NewGauge(name string) metrics.Gauge {
21 return expvar.NewGauge(name)
22 }
23
24 // NewHistogram implements Provider.
25 func (p expvarProvider) NewHistogram(name string, buckets int) metrics.Histogram {
26 return expvar.NewHistogram(name, buckets)
27 }
28
29 // Stop implements Provider, but is a no-op.
30 func (p expvarProvider) Stop() {}
+0
-41
metrics3/provider/graphite.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/graphite"
5 )
6
7 type graphiteProvider struct {
8 g *graphite.Graphite
9 stop func()
10 }
11
12 // NewGraphiteProvider wraps the given Graphite object and stop func and returns
13 // a Provider that produces Graphite metrics. A typical stop function would be
14 // ticker.Stop from the ticker passed to the SendLoop helper method.
15 func NewGraphiteProvider(g *graphite.Graphite, stop func()) Provider {
16 return &graphiteProvider{
17 g: g,
18 stop: stop,
19 }
20 }
21
22 // NewCounter implements Provider.
23 func (p *graphiteProvider) NewCounter(name string) metrics.Counter {
24 return p.g.NewCounter(name)
25 }
26
27 // NewGauge implements Provider.
28 func (p *graphiteProvider) NewGauge(name string) metrics.Gauge {
29 return p.g.NewGauge(name)
30 }
31
32 // NewHistogram implements Provider.
33 func (p *graphiteProvider) NewHistogram(name string, buckets int) metrics.Histogram {
34 return p.g.NewHistogram(name, buckets)
35 }
36
37 // Stop implements Provider, invoking the stop function passed at construction.
38 func (p *graphiteProvider) Stop() {
39 p.stop()
40 }
+0
-40
metrics3/provider/influx.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/influx"
5 )
6
7 type influxProvider struct {
8 in *influx.Influx
9 stop func()
10 }
11
12 // NewInfluxProvider takes the given Influx object and stop func, and returns
13 // a Provider that produces Influx metrics.
14 func NewInfluxProvider(in *influx.Influx, stop func()) Provider {
15 return &influxProvider{
16 in: in,
17 stop: stop,
18 }
19 }
20
21 // NewCounter implements Provider. Per-metric tags are not supported.
22 func (p *influxProvider) NewCounter(name string) metrics.Counter {
23 return p.in.NewCounter(name)
24 }
25
26 // NewGauge implements Provider. Per-metric tags are not supported.
27 func (p *influxProvider) NewGauge(name string) metrics.Gauge {
28 return p.in.NewGauge(name)
29 }
30
31 // NewHistogram implements Provider. Per-metric tags are not supported.
32 func (p *influxProvider) NewHistogram(name string, buckets int) metrics.Histogram {
33 return p.in.NewHistogram(name)
34 }
35
36 // Stop implements Provider, invoking the stop function passed at construction.
37 func (p *influxProvider) Stop() {
38 p.stop()
39 }
+0
-63
metrics3/provider/prometheus.go less more
0 package provider
1
2 import (
3 stdprometheus "github.com/prometheus/client_golang/prometheus"
4
5 "github.com/go-kit/kit/metrics3"
6 "github.com/go-kit/kit/metrics3/prometheus"
7 )
8
9 type prometheusProvider struct {
10 namespace string
11 subsystem string
12 }
13
14 // NewPrometheusProvider returns a Provider that produces Prometheus metrics.
15 // Namespace and subsystem are applied to all produced metrics.
16 func NewPrometheusProvider(namespace, subsystem string) Provider {
17 return &prometheusProvider{
18 namespace: namespace,
19 subsystem: subsystem,
20 }
21 }
22
23 // NewCounter implements Provider via prometheus.NewCounterFrom, i.e. the
24 // counter is registered. The metric's namespace and subsystem are taken from
25 // the Provider. Help is set to the name of the metric, and no const label names
26 // are set.
27 func (p *prometheusProvider) NewCounter(name string) metrics.Counter {
28 return prometheus.NewCounterFrom(stdprometheus.CounterOpts{
29 Namespace: p.namespace,
30 Subsystem: p.subsystem,
31 Name: name,
32 Help: name,
33 }, []string{})
34 }
35
36 // NewGauge implements Provider via prometheus.NewGaugeFrom, i.e. the gauge is
37 // registered. The metric's namespace and subsystem are taken from the Provider.
38 // Help is set to the name of the metric, and no const label names are set.
39 func (p *prometheusProvider) NewGauge(name string) metrics.Gauge {
40 return prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
41 Namespace: p.namespace,
42 Subsystem: p.subsystem,
43 Name: name,
44 Help: name,
45 }, []string{})
46 }
47
48 // NewGauge implements Provider via prometheus.NewSummaryFrom, i.e. the summary
49 // is registered. The metric's namespace and subsystem are taken from the
50 // Provider. Help is set to the name of the metric, and no const label names are
51 // set. Buckets are ignored.
52 func (p *prometheusProvider) NewHistogram(name string, _ int) metrics.Histogram {
53 return prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
54 Namespace: p.namespace,
55 Subsystem: p.subsystem,
56 Name: name,
57 Help: name,
58 }, []string{})
59 }
60
61 // Stop implements Provider, but is a no-op.
62 func (p *prometheusProvider) Stop() {}
+0
-42
metrics3/provider/provider.go less more
0 // Package provider provides a factory-like abstraction for metrics backends.
1 // This package is provided specifically for the needs of the NY Times framework
2 // Gizmo. Most normal Go kit users shouldn't need to use it.
3 //
4 // Normally, if your microservice needs to support different metrics backends,
5 // you can simply do different construction based on a flag. For example,
6 //
7 // var latency metrics.Histogram
8 // var requests metrics.Counter
9 // switch *metricsBackend {
10 // case "prometheus":
11 // latency = prometheus.NewSummaryVec(...)
12 // requests = prometheus.NewCounterVec(...)
13 // case "statsd":
14 // s := statsd.New(...)
15 // t := time.NewTicker(5*time.Second)
16 // go s.SendLoop(t.C, "tcp", "statsd.local:8125")
17 // latency = s.NewHistogram(...)
18 // requests = s.NewCounter(...)
19 // default:
20 // log.Fatal("unsupported metrics backend %q", *metricsBackend)
21 // }
22 //
23 package provider
24
25 import (
26 "github.com/go-kit/kit/metrics3"
27 )
28
29 // Provider abstracts over constructors and lifecycle management functions for
30 // each supported metrics backend. It should only be used by those who need to
31 // swap out implementations dynamically.
32 //
33 // This is primarily useful for intermediating frameworks, and is likely
34 // unnecessary for most Go kit services. See the package-level doc comment for
35 // more typical usage instructions.
36 type Provider interface {
37 NewCounter(name string) metrics.Counter
38 NewGauge(name string) metrics.Gauge
39 NewHistogram(name string, buckets int) metrics.Histogram
40 Stop()
41 }
+0
-43
metrics3/provider/statsd.go less more
0 package provider
1
2 import (
3 "github.com/go-kit/kit/metrics3"
4 "github.com/go-kit/kit/metrics3/statsd"
5 )
6
7 type statsdProvider struct {
8 s *statsd.Statsd
9 stop func()
10 }
11
12 // NewStatsdProvider wraps the given Statsd object and stop func and returns a
13 // Provider that produces Statsd metrics. A typical stop function would be
14 // ticker.Stop from the ticker passed to the SendLoop helper method.
15 func NewStatsdProvider(s *statsd.Statsd, stop func()) Provider {
16 return &statsdProvider{
17 s: s,
18 stop: stop,
19 }
20 }
21
22 // NewCounter implements Provider.
23 func (p *statsdProvider) NewCounter(name string) metrics.Counter {
24 return p.s.NewCounter(name, 1.0)
25 }
26
27 // NewGauge implements Provider.
28 func (p *statsdProvider) NewGauge(name string) metrics.Gauge {
29 return p.s.NewGauge(name)
30 }
31
32 // NewHistogram implements Provider, returning a StatsD Timing that accepts
33 // observations in milliseconds. The sample rate is fixed at 1.0. The bucket
34 // parameter is ignored.
35 func (p *statsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
36 return p.s.NewTiming(name, 1.0)
37 }
38
39 // Stop implements Provider, invoking the stop function passed at construction.
40 func (p *statsdProvider) Stop() {
41 p.stop()
42 }
+0
-232
metrics3/statsd/statsd.go less more
0 // Package statsd provides a StatsD backend for package metrics. StatsD has no
1 // concept of arbitrary key-value tagging, so label values are not supported,
2 // and With is a no-op on all metrics.
3 //
4 // This package batches observations and emits them on some schedule to the
5 // remote server. This is useful even if you connect to your StatsD server over
6 // UDP. Emitting one network packet per observation can quickly overwhelm even
7 // the fastest internal network.
8 package statsd
9
10 import (
11 "fmt"
12 "io"
13 "time"
14
15 "github.com/go-kit/kit/log"
16 "github.com/go-kit/kit/metrics3"
17 "github.com/go-kit/kit/metrics3/internal/lv"
18 "github.com/go-kit/kit/metrics3/internal/ratemap"
19 "github.com/go-kit/kit/util/conn"
20 )
21
22 // Statsd receives metrics observations and forwards them to a StatsD server.
23 // Create a Statsd object, use it to create metrics, and pass those metrics as
24 // dependencies to the components that will use them.
25 //
26 // All metrics are buffered until WriteTo is called. Counters and gauges are
27 // aggregated into a single observation per timeseries per write. Timings are
28 // buffered but not aggregated.
29 //
30 // To regularly report metrics to an io.Writer, use the WriteLoop helper method.
31 // To send to a StatsD server, use the SendLoop helper method.
32 type Statsd struct {
33 prefix string
34 rates *ratemap.RateMap
35
36 // The observations are collected in an N-dimensional vector space, even
37 // though they only take advantage of a single dimension (name). This is an
38 // implementation detail born purely from convenience. It would be more
39 // accurate to collect them in a map[string][]float64, but we already have
40 // this nice data structure and helper methods.
41 counters *lv.Space
42 gauges *lv.Space
43 timings *lv.Space
44
45 logger log.Logger
46 }
47
48 // New returns a Statsd object that may be used to create metrics. Prefix is
49 // applied to all created metrics. Callers must ensure that regular calls to
50 // WriteTo are performed, either manually or with one of the helper methods.
51 func New(prefix string, logger log.Logger) *Statsd {
52 return &Statsd{
53 prefix: prefix,
54 rates: ratemap.New(),
55 counters: lv.NewSpace(),
56 gauges: lv.NewSpace(),
57 timings: lv.NewSpace(),
58 logger: logger,
59 }
60 }
61
62 // NewCounter returns a counter, sending observations to this Statsd object.
63 func (s *Statsd) NewCounter(name string, sampleRate float64) *Counter {
64 s.rates.Set(s.prefix+name, sampleRate)
65 return &Counter{
66 name: s.prefix + name,
67 obs: s.counters.Observe,
68 }
69 }
70
71 // NewGauge returns a gauge, sending observations to this Statsd object.
72 func (s *Statsd) NewGauge(name string) *Gauge {
73 return &Gauge{
74 name: s.prefix + name,
75 obs: s.gauges.Observe,
76 }
77 }
78
79 // NewTiming returns a histogram whose observations are interpreted as
80 // millisecond durations, and are forwarded to this Statsd object.
81 func (s *Statsd) NewTiming(name string, sampleRate float64) *Timing {
82 s.rates.Set(s.prefix+name, sampleRate)
83 return &Timing{
84 name: s.prefix + name,
85 obs: s.timings.Observe,
86 }
87 }
88
89 // WriteLoop is a helper method that invokes WriteTo to the passed writer every
90 // time the passed channel fires. This method blocks until the channel is
91 // closed, so clients probably want to run it in its own goroutine. For typical
92 // usage, create a time.Ticker and pass its C channel to this method.
93 func (s *Statsd) WriteLoop(c <-chan time.Time, w io.Writer) {
94 for range c {
95 if _, err := s.WriteTo(w); err != nil {
96 s.logger.Log("during", "WriteTo", "err", err)
97 }
98 }
99 }
100
101 // SendLoop is a helper method that wraps WriteLoop, passing a managed
102 // connection to the network and address. Like WriteLoop, this method blocks
103 // until the channel is closed, so clients probably want to start it in its own
104 // goroutine. For typical usage, create a time.Ticker and pass its C channel to
105 // this method.
106 func (s *Statsd) SendLoop(c <-chan time.Time, network, address string) {
107 s.WriteLoop(c, conn.NewDefaultManager(network, address, s.logger))
108 }
109
110 // WriteTo flushes the buffered content of the metrics to the writer, in
111 // StatsD format. WriteTo abides best-effort semantics, so observations are
112 // lost if there is a problem with the write. Clients should be sure to call
113 // WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
114 func (s *Statsd) WriteTo(w io.Writer) (count int64, err error) {
115 var n int
116
117 s.counters.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
118 n, err = fmt.Fprintf(w, "%s:%f|c%s\n", name, sum(values), sampling(s.rates.Get(name)))
119 if err != nil {
120 return false
121 }
122 count += int64(n)
123 return true
124 })
125 if err != nil {
126 return count, err
127 }
128
129 s.gauges.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
130 n, err = fmt.Fprintf(w, "%s:%f|g\n", name, last(values))
131 if err != nil {
132 return false
133 }
134 count += int64(n)
135 return true
136 })
137 if err != nil {
138 return count, err
139 }
140
141 s.timings.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
142 sampleRate := s.rates.Get(name)
143 for _, value := range values {
144 n, err = fmt.Fprintf(w, "%s:%f|ms%s\n", name, value, sampling(sampleRate))
145 if err != nil {
146 return false
147 }
148 count += int64(n)
149 }
150 return true
151 })
152 if err != nil {
153 return count, err
154 }
155
156 return count, err
157 }
158
159 func sum(a []float64) float64 {
160 var v float64
161 for _, f := range a {
162 v += f
163 }
164 return v
165 }
166
167 func last(a []float64) float64 {
168 return a[len(a)-1]
169 }
170
171 func sampling(r float64) string {
172 var sv string
173 if r < 1.0 {
174 sv = fmt.Sprintf("|@%f", r)
175 }
176 return sv
177 }
178
179 type observeFunc func(name string, lvs lv.LabelValues, value float64)
180
181 // Counter is a StatsD counter. Observations are forwarded to a Statsd object,
182 // and aggregated (summed) per timeseries.
183 type Counter struct {
184 name string
185 obs observeFunc
186 }
187
188 // With is a no-op.
189 func (c *Counter) With(...string) metrics.Counter {
190 return c
191 }
192
193 // Add implements metrics.Counter.
194 func (c *Counter) Add(delta float64) {
195 c.obs(c.name, lv.LabelValues{}, delta)
196 }
197
198 // Gauge is a StatsD gauge. Observations are forwarded to a Statsd object, and
199 // aggregated (the last observation selected) per timeseries.
200 type Gauge struct {
201 name string
202 obs observeFunc
203 }
204
205 // With is a no-op.
206 func (g *Gauge) With(...string) metrics.Gauge {
207 return g
208 }
209
210 // Set implements metrics.Gauge.
211 func (g *Gauge) Set(value float64) {
212 g.obs(g.name, lv.LabelValues{}, value)
213 }
214
215 // Timing is a StatsD timing, or metrics.Histogram. Observations are
216 // forwarded to a Statsd object, and collected (but not aggregated) per
217 // timeseries.
218 type Timing struct {
219 name string
220 obs observeFunc
221 }
222
223 // With is a no-op.
224 func (t *Timing) With(...string) metrics.Histogram {
225 return t
226 }
227
228 // Observe implements metrics.Histogram. Value is interpreted as milliseconds.
229 func (t *Timing) Observe(value float64) {
230 t.obs(t.name, lv.LabelValues{}, value)
231 }
+0
-66
metrics3/statsd/statsd_test.go less more
0 package statsd
1
2 import (
3 "testing"
4
5 "github.com/go-kit/kit/log"
6 "github.com/go-kit/kit/metrics3/teststat"
7 )
8
9 func TestCounter(t *testing.T) {
10 prefix, name := "abc.", "def"
11 label, value := "label", "value" // ignored
12 regex := `^` + prefix + name + `:([0-9\.]+)\|c$`
13 s := New(prefix, log.NewNopLogger())
14 counter := s.NewCounter(name, 1.0).With(label, value)
15 valuef := teststat.SumLines(s, regex)
16 if err := teststat.TestCounter(counter, valuef); err != nil {
17 t.Fatal(err)
18 }
19 }
20
21 func TestCounterSampled(t *testing.T) {
22 // This will involve multiplying the observed sum by the inverse of the
23 // sample rate and checking against the expected value within some
24 // tolerance.
25 t.Skip("TODO")
26 }
27
28 func TestGauge(t *testing.T) {
29 prefix, name := "ghi.", "jkl"
30 label, value := "xyz", "abc" // ignored
31 regex := `^` + prefix + name + `:([0-9\.]+)\|g$`
32 s := New(prefix, log.NewNopLogger())
33 gauge := s.NewGauge(name).With(label, value)
34 valuef := teststat.LastLine(s, regex)
35 if err := teststat.TestGauge(gauge, valuef); err != nil {
36 t.Fatal(err)
37 }
38 }
39
40 // StatsD timings just emit all observations. So, we collect them into a generic
41 // histogram, and run the statistics test on that.
42
43 func TestTiming(t *testing.T) {
44 prefix, name := "statsd.", "timing_test"
45 label, value := "abc", "def" // ignored
46 regex := `^` + prefix + name + `:([0-9\.]+)\|ms$`
47 s := New(prefix, log.NewNopLogger())
48 timing := s.NewTiming(name, 1.0).With(label, value)
49 quantiles := teststat.Quantiles(s, regex, 50) // no |@0.X
50 if err := teststat.TestHistogram(timing, quantiles, 0.01); err != nil {
51 t.Fatal(err)
52 }
53 }
54
55 func TestTimingSampled(t *testing.T) {
56 prefix, name := "statsd.", "sampled_timing_test"
57 label, value := "foo", "bar" // ignored
58 regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0\.01[0]*$`
59 s := New(prefix, log.NewNopLogger())
60 timing := s.NewTiming(name, 0.01).With(label, value)
61 quantiles := teststat.Quantiles(s, regex, 50)
62 if err := teststat.TestHistogram(timing, quantiles, 0.02); err != nil {
63 t.Fatal(err)
64 }
65 }
+0
-65
metrics3/teststat/buffers.go less more
0 package teststat
1
2 import (
3 "bufio"
4 "bytes"
5 "io"
6 "regexp"
7 "strconv"
8
9 "github.com/go-kit/kit/metrics3/generic"
10 )
11
12 // SumLines expects a regex whose first capture group can be parsed as a
13 // float64. It will dump the WriterTo and parse each line, expecting to find a
14 // match. It returns the sum of all captured floats.
15 func SumLines(w io.WriterTo, regex string) func() float64 {
16 return func() float64 {
17 sum, _ := stats(w, regex, nil)
18 return sum
19 }
20 }
21
22 // LastLine expects a regex whose first capture group can be parsed as a
23 // float64. It will dump the WriterTo and parse each line, expecting to find a
24 // match. It returns the final captured float.
25 func LastLine(w io.WriterTo, regex string) func() float64 {
26 return func() float64 {
27 _, final := stats(w, regex, nil)
28 return final
29 }
30 }
31
32 // Quantiles expects a regex whose first capture group can be parsed as a
33 // float64. It will dump the WriterTo and parse each line, expecting to find a
34 // match. It observes all captured floats into a generic.Histogram with the
35 // given number of buckets, and returns the 50th, 90th, 95th, and 99th quantiles
36 // from that histogram.
37 func Quantiles(w io.WriterTo, regex string, buckets int) func() (float64, float64, float64, float64) {
38 return func() (float64, float64, float64, float64) {
39 h := generic.NewHistogram("quantile-test", buckets)
40 stats(w, regex, h)
41 return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99)
42 }
43 }
44
45 func stats(w io.WriterTo, regex string, h *generic.Histogram) (sum, final float64) {
46 re := regexp.MustCompile(regex)
47 buf := &bytes.Buffer{}
48 w.WriteTo(buf)
49 //fmt.Fprintf(os.Stderr, "%s\n", buf.String())
50 s := bufio.NewScanner(buf)
51 for s.Scan() {
52 match := re.FindStringSubmatch(s.Text())
53 f, err := strconv.ParseFloat(match[1], 64)
54 if err != nil {
55 panic(err)
56 }
57 sum += f
58 final = f
59 if h != nil {
60 h.Observe(f)
61 }
62 }
63 return sum, final
64 }
+0
-72
metrics3/teststat/populate.go less more
0 package teststat
1
2 import (
3 "math"
4 "math/rand"
5
6 "github.com/go-kit/kit/metrics3"
7 )
8
9 // PopulateNormalHistogram makes a series of normal random observations into the
10 // histogram. The number of observations is determined by Count. The randomness
11 // is determined by Mean, Stdev, and the seed parameter.
12 //
13 // This is a low-level function, exported only for metrics that don't perform
14 // dynamic quantile computation, like a Prometheus Histogram (c.f. Summary). In
15 // most cases, you don't need to use this function, and can use TestHistogram
16 // instead.
17 func PopulateNormalHistogram(h metrics.Histogram, seed int) {
18 r := rand.New(rand.NewSource(int64(seed)))
19 for i := 0; i < Count; i++ {
20 sample := r.NormFloat64()*float64(Stdev) + float64(Mean)
21 if sample < 0 {
22 sample = 0
23 }
24 h.Observe(sample)
25 }
26 }
27
28 func normalQuantiles() (p50, p90, p95, p99 float64) {
29 return nvq(50), nvq(90), nvq(95), nvq(99)
30 }
31
32 func nvq(quantile int) float64 {
33 // https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
34 return float64(Mean) + float64(Stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)
35 }
36
37 func erfinv(y float64) float64 {
38 // https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
39 if y < -1.0 || y > 1.0 {
40 panic("invalid input")
41 }
42
43 var (
44 a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
45 b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
46 c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
47 d = [2]float64{3.543889200, 1.637067800}
48 )
49
50 const y0 = 0.7
51 var x, z float64
52
53 if math.Abs(y) == 1.0 {
54 x = -y * math.Log(0.0)
55 } else if y < -y0 {
56 z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
57 x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
58 } else {
59 if y < y0 {
60 z = y * y
61 x = y * (((a[3]*z+a[2])*z+a[1])*z + a[0]) / ((((b[3]*z+b[3])*z+b[1])*z+b[0])*z + 1.0)
62 } else {
63 z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
64 x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
65 }
66 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
67 x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
68 }
69
70 return x
71 }
+0
-103
metrics3/teststat/teststat.go less more
0 // Package teststat provides helpers for testing metrics backends.
1 package teststat
2
3 import (
4 "errors"
5 "fmt"
6 "math"
7 "math/rand"
8 "strings"
9
10 "github.com/go-kit/kit/metrics3"
11 )
12
13 // TestCounter puts some deltas through the counter, and then calls the value
14 // func to check that the counter has the correct final value.
15 func TestCounter(counter metrics.Counter, value func() float64) error {
16 a := rand.Perm(100)
17 n := rand.Intn(len(a))
18
19 var want float64
20 for i := 0; i < n; i++ {
21 f := float64(a[i])
22 counter.Add(f)
23 want += f
24 }
25
26 if have := value(); want != have {
27 return fmt.Errorf("want %f, have %f", want, have)
28 }
29
30 return nil
31 }
32
33 // TestGauge puts some values through the gauge, and then calls the value func
34 // to check that the gauge has the correct final value.
35 func TestGauge(gauge metrics.Gauge, value func() float64) error {
36 a := rand.Perm(100)
37 n := rand.Intn(len(a))
38
39 var want float64
40 for i := 0; i < n; i++ {
41 f := float64(a[i])
42 gauge.Set(f)
43 want = f
44 }
45
46 if have := value(); want != have {
47 return fmt.Errorf("want %f, have %f", want, have)
48 }
49
50 return nil
51 }
52
53 // TestHistogram puts some observations through the histogram, and then calls
54 // the quantiles func to checks that the histogram has computed the correct
55 // quantiles within some tolerance
56 func TestHistogram(histogram metrics.Histogram, quantiles func() (p50, p90, p95, p99 float64), tolerance float64) error {
57 PopulateNormalHistogram(histogram, rand.Int())
58
59 want50, want90, want95, want99 := normalQuantiles()
60 have50, have90, have95, have99 := quantiles()
61
62 var errs []string
63 if want, have := want50, have50; !cmp(want, have, tolerance) {
64 errs = append(errs, fmt.Sprintf("p50: want %f, have %f", want, have))
65 }
66 if want, have := want90, have90; !cmp(want, have, tolerance) {
67 errs = append(errs, fmt.Sprintf("p90: want %f, have %f", want, have))
68 }
69 if want, have := want95, have95; !cmp(want, have, tolerance) {
70 errs = append(errs, fmt.Sprintf("p95: want %f, have %f", want, have))
71 }
72 if want, have := want99, have99; !cmp(want, have, tolerance) {
73 errs = append(errs, fmt.Sprintf("p99: want %f, have %f", want, have))
74 }
75 if len(errs) > 0 {
76 return errors.New(strings.Join(errs, "; "))
77 }
78
79 return nil
80 }
81
82 var (
83 Count = 12345
84 Mean = 500
85 Stdev = 25
86 )
87
88 // ExpectedObservationsLessThan returns the number of observations that should
89 // have a value less than or equal to the given value, given a normal
90 // distribution of observations described by Count, Mean, and Stdev.
91 func ExpectedObservationsLessThan(bucket int64) int64 {
92 // https://code.google.com/p/gostat/source/browse/stat/normal.go
93 cdf := ((1.0 / 2.0) * (1 + math.Erf((float64(bucket)-float64(Mean))/(float64(Stdev)*math.Sqrt2))))
94 return int64(cdf * float64(Count))
95 }
96
97 func cmp(want, have, tol float64) bool {
98 if (math.Abs(want-have) / want) > tol {
99 return false
100 }
101 return true
102 }