mv metrics3 metrics
Peter Bourgon
7 years ago
0 | 0 | # package metrics |
1 | 1 | |
2 | 2 | `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). | |
12 | 11 | |
13 | 12 | ## Rationale |
14 | 13 | |
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. | |
16 | 17 | 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. | |
20 | 19 | |
21 | 20 | ## Usage |
22 | 21 | |
31 | 30 | } |
32 | 31 | ``` |
33 | 32 | |
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. | |
36 | 35 | |
37 | 36 | ```go |
38 | 37 | import ( |
42 | 41 | "github.com/go-kit/kit/metrics/prometheus" |
43 | 42 | ) |
44 | 43 | |
45 | var requestDuration = prometheus.NewSummary(stdprometheus.SummaryOpts{ | |
44 | var dur = prometheus.NewSummary(stdprometheus.SummaryOpts{ | |
46 | 45 | Namespace: "myservice", |
47 | 46 | 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.", | |
50 | 49 | }, []string{}) |
51 | 50 | |
52 | 51 | 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()) | |
54 | 53 | // handle request |
55 | 54 | } |
56 | 55 | ``` |
57 | 56 | |
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. | |
59 | 58 | |
60 | 59 | ```go |
61 | 60 | import ( |
65 | 64 | "time" |
66 | 65 | |
67 | 66 | "github.com/go-kit/kit/metrics/statsd" |
67 | "github.com/go-kit/kit/log" | |
68 | 68 | ) |
69 | 69 | |
70 | 70 | 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()) | |
75 | 72 | |
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) { | |
79 | 79 | goroutines.Set(float64(runtime.NumGoroutine())) |
80 | 80 | } |
81 | 81 | } |
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. | |
2 | 1 | package discard |
3 | 2 | |
4 | import "github.com/go-kit/kit/metrics" | |
3 | import "github.com/go-kit/kit/metrics3" | |
5 | 4 | |
6 | type counter struct { | |
7 | name string | |
8 | } | |
5 | type counter struct{} | |
9 | 6 | |
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{} } | |
12 | 9 | |
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 } | |
16 | 12 | |
17 | type gauge struct { | |
18 | name string | |
19 | } | |
13 | // Add implements Counter. | |
14 | func (c counter) Add(delta float64) {} | |
20 | 15 | |
21 | // NewGauge returns a Gauge that does nothing. | |
22 | func NewGauge(name string) metrics.Gauge { return &gauge{name} } | |
16 | type gauge struct{} | |
23 | 17 | |
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{} } | |
29 | 20 | |
30 | type histogram struct { | |
31 | name string | |
32 | } | |
21 | // With implements Gauge. | |
22 | func (g gauge) With(labelValues ...string) metrics.Gauge { return g } | |
33 | 23 | |
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) {} | |
36 | 26 | |
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) {} |
0 | 0 | // Package metrics provides a framework for application instrumentation. All |
1 | 1 | // metrics are safe for concurrent use. Considerable design influence has been |
2 | 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 | // | |
3 | 58 | 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/. | |
1 | 5 | // |
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. | |
7 | 10 | package dogstatsd |
8 | 11 | |
9 | 12 | import ( |
10 | "bytes" | |
11 | 13 | "fmt" |
12 | 14 | "io" |
13 | "log" | |
14 | "math" | |
15 | "strings" | |
15 | 16 | "time" |
16 | 17 | |
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" | |
20 | 23 | ) |
21 | 24 | |
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. | |
35 | 28 | // |
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. | |
75 | 32 | // |
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 | |
215 | 155 | } |
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 | |
242 | 170 | } |
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 | } |
0 | 0 | package dogstatsd |
1 | 1 | |
2 | 2 | import ( |
3 | "bytes" | |
4 | "fmt" | |
5 | "net" | |
6 | "strings" | |
7 | "sync" | |
8 | 3 | "testing" |
9 | "time" | |
10 | 4 | |
11 | 5 | "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" | |
14 | 7 | ) |
15 | 8 | |
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) | |
32 | 18 | } |
33 | 19 | } |
34 | 20 | |
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 | } | |
37 | 27 | |
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) | |
52 | 37 | } |
53 | 38 | } |
54 | 39 | |
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. | |
58 | 42 | |
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) | |
70 | 52 | } |
71 | 53 | } |
72 | 54 | |
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) | |
186 | 64 | } |
187 | 65 | } |
188 | 66 | |
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) | |
228 | 76 | } |
229 | 77 | } |
230 | 78 | |
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 | } | |
233 | 89 | } |
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 | 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. | |
16 | 2 | package expvar |
17 | 3 | |
18 | 4 | import ( |
19 | 5 | "expvar" |
20 | "fmt" | |
21 | "sort" | |
22 | "strconv" | |
23 | 6 | "sync" |
24 | "time" | |
25 | 7 | |
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" | |
29 | 10 | ) |
30 | 11 | |
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 | |
34 | 16 | } |
35 | 17 | |
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), | |
42 | 23 | } |
43 | 24 | } |
44 | 25 | |
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 } | |
48 | 28 | |
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 | |
52 | 36 | } |
53 | 37 | |
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), | |
61 | 43 | } |
62 | 44 | } |
63 | 45 | |
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 } | |
69 | 48 | |
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 | |
76 | 63 | } |
77 | 64 | |
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"), | |
127 | 75 | } |
128 | 76 | } |
129 | 77 | |
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)) | |
149 | 90 | } |
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 | |
1 | 1 | |
2 | 2 | import ( |
3 | stdexpvar "expvar" | |
4 | "fmt" | |
3 | "strconv" | |
5 | 4 | "testing" |
6 | 5 | |
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" | |
10 | 7 | ) |
11 | 8 | |
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 | ||
34 | 9 | 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) | |
42 | 14 | } |
43 | 15 | } |
44 | 16 | |
45 | 17 | 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) | |
56 | 22 | } |
57 | 23 | } |
58 | 24 | |
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 | } | |
68 | 37 | } |
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 | 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 | |
3 | 3 | // |
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. | |
8 | 7 | package graphite |
9 | 8 | |
10 | 9 | import ( |
11 | 10 | "fmt" |
12 | 11 | "io" |
13 | "math" | |
14 | "sort" | |
15 | 12 | "sync" |
16 | "sync/atomic" | |
17 | 13 | "time" |
18 | 14 | |
19 | "github.com/codahale/hdrhistogram" | |
20 | ||
21 | 15 | "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" | |
23 | 19 | ) |
24 | 20 | |
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. | |
95 | 24 | // |
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. | |
99 | 28 | // |
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() | |
119 | 81 | return h |
120 | 82 | } |
121 | 83 | |
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() | |
165 | 113 | 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) } |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "bytes" |
4 | "fmt" | |
5 | "strings" | |
4 | "regexp" | |
5 | "strconv" | |
6 | 6 | "testing" |
7 | "time" | |
8 | 7 | |
9 | 8 | "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" | |
12 | 10 | ) |
13 | 11 | |
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 | ||
35 | 12 | 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) | |
49 | 21 | } |
50 | 22 | } |
51 | 23 | |
52 | 24 | 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) | |
72 | 33 | } |
73 | 34 | } |
74 | 35 | |
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 | } | |
79 | 62 | } |
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 | // 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 | 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 | } |
0 | 0 | package metrics |
1 | 1 | |
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. | |
6 | 4 | type Counter interface { |
7 | Name() string | |
8 | With(Field) Counter | |
9 | Add(delta uint64) | |
5 | With(labelValues ...string) Counter | |
6 | Add(delta float64) | |
10 | 7 | } |
11 | 8 | |
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. | |
14 | 11 | type Gauge interface { |
15 | Name() string | |
16 | With(Field) Gauge | |
12 | With(labelValues ...string) Gauge | |
17 | 13 | Set(value float64) |
18 | Add(delta float64) | |
19 | Get() float64 | |
20 | 14 | } |
21 | 15 | |
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. | |
25 | 20 | 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) | |
30 | 23 | } |
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 | 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 | 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 | 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 | 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. | |
1 | 4 | package prometheus |
2 | 5 | |
3 | 6 | import ( |
4 | 7 | "github.com/prometheus/client_golang/prometheus" |
5 | 8 | |
6 | "github.com/go-kit/kit/metrics" | |
9 | "github.com/go-kit/kit/metrics3" | |
10 | "github.com/go-kit/kit/metrics3/internal/lv" | |
7 | 11 | ) |
8 | 12 | |
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 | |
20 | 17 | } |
21 | 18 | |
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, | |
35 | 31 | } |
36 | 32 | } |
37 | 33 | |
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...), | |
45 | 39 | } |
46 | 40 | } |
47 | 41 | |
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) | |
50 | 45 | } |
51 | 46 | |
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 | |
56 | 51 | } |
57 | 52 | |
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, | |
67 | 65 | } |
68 | 66 | } |
69 | 67 | |
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...), | |
77 | 73 | } |
78 | 74 | } |
79 | 75 | |
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) | |
82 | 79 | } |
83 | 80 | |
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) | |
86 | 84 | } |
87 | 85 | |
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 | |
91 | 92 | } |
92 | 93 | |
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) | |
99 | 100 | } |
100 | 101 | |
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, | |
119 | 106 | } |
120 | 107 | } |
121 | 108 | |
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...), | |
129 | 114 | } |
130 | 115 | } |
131 | 116 | |
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) | |
134 | 120 | } |
135 | 121 | |
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 | |
139 | 128 | } |
140 | 129 | |
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) | |
145 | 136 | } |
146 | 137 | |
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, | |
159 | 142 | } |
160 | 143 | } |
161 | 144 | |
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...), | |
169 | 150 | } |
170 | 151 | } |
171 | 152 | |
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) | |
174 | 156 | } |
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 | |
1 | 1 | |
2 | 2 | import ( |
3 | "io/ioutil" | |
4 | "math" | |
5 | "math/rand" | |
6 | "net/http" | |
7 | "net/http/httptest" | |
8 | "regexp" | |
9 | "strconv" | |
3 | 10 | "strings" |
4 | 11 | "testing" |
5 | 12 | |
13 | "github.com/go-kit/kit/metrics3/teststat" | |
6 | 14 | 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" | |
11 | 15 | ) |
12 | 16 | |
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() | |
22 | 20 | |
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) | |
30 | 45 | } |
31 | 46 | } |
32 | 47 | |
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.", | |
39 | 66 | }, []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 | |
48 | 72 | } |
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) | |
57 | 76 | } |
58 | 77 | } |
59 | 78 | |
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.", | |
66 | 99 | }, []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 | |
74 | 111 | } |
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) | |
82 | 115 | } |
83 | 116 | } |
84 | 117 | |
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 | } | |
100 | 185 | } |
101 | 186 | } |
102 | 187 | |
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") | |
114 | 190 | } |
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 | 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 | 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 | 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 | 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 | 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. | |
1 | 3 | // |
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. | |
12 | 8 | package statsd |
13 | 9 | |
14 | 10 | import ( |
15 | "bytes" | |
16 | 11 | "fmt" |
17 | 12 | "io" |
18 | "log" | |
19 | "math" | |
20 | 13 | "time" |
21 | 14 | |
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" | |
25 | 20 | ) |
26 | 21 | |
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. | |
41 | 25 | // |
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 { | |
55 | 190 | return c |
56 | 191 | } |
57 | 192 | |
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 { | |
88 | 207 | return g |
89 | 208 | } |
90 | 209 | |
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 | } |
0 | 0 | package statsd |
1 | 1 | |
2 | 2 | import ( |
3 | "bytes" | |
4 | "fmt" | |
5 | "net" | |
6 | "strings" | |
7 | "sync" | |
8 | 3 | "testing" |
9 | "time" | |
10 | 4 | |
11 | 5 | "github.com/go-kit/kit/log" |
12 | "github.com/go-kit/kit/util/conn" | |
6 | "github.com/go-kit/kit/metrics3/teststat" | |
13 | 7 | ) |
14 | 8 | |
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) | |
31 | 18 | } |
32 | 19 | } |
33 | 20 | |
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 | } | |
36 | 27 | |
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) | |
51 | 37 | } |
52 | 38 | } |
53 | 39 | |
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. | |
57 | 42 | |
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) | |
69 | 52 | } |
70 | 53 | } |
71 | 54 | |
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) | |
179 | 64 | } |
180 | 65 | } |
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 | 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 | // 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 | 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 | 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 | 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 | 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 | 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 | # 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 | // 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 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 | // 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 | // 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 | 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 | // 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 | 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 | // 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 | // 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 | 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 | // 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 | 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 | } |
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 | // 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 | 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 | 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 | 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 | // 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 | 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 | 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 | 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 | // 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 | } |