Codebase list golang-github-go-kit-kit / c8beefd
metrics: add Circonus backend Peter Bourgon 7 years ago
3 changed file(s) with 344 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 // Package circonus provides a Circonus backend for package metrics.
1 //
2 // Users are responsible for calling the circonusgometrics.Start method
3 // themselves. Note that all Circonus metrics must have unique names, and are
4 // registered in a package-global registry. Circonus metrics also don't support
5 // fields, so all With methods are no-ops.
6 package circonus
7
8 import (
9 "github.com/circonus-labs/circonus-gometrics"
10
11 "github.com/go-kit/kit/metrics"
12 )
13
14 // NewCounter returns a counter backed by a Circonus counter with the given
15 // name. Due to the Circonus data model, fields are not supported.
16 func NewCounter(name string) metrics.Counter {
17 return counter(name)
18 }
19
20 type counter circonusgometrics.Counter
21
22 // Name implements Counter.
23 func (c counter) Name() string {
24 return string(c)
25 }
26
27 // With implements Counter, but is a no-op.
28 func (c counter) With(metrics.Field) metrics.Counter {
29 return c
30 }
31
32 // Add implements Counter.
33 func (c counter) Add(delta uint64) {
34 circonusgometrics.Counter(c).AddN(delta)
35 }
36
37 // NewGauge returns a gauge backed by a Circonus gauge with the given name. Due
38 // to the Circonus data model, fields are not supported. Also, Circonus gauges
39 // are defined as integers, so values are truncated.
40 func NewGauge(name string) metrics.Gauge {
41 return gauge(name)
42 }
43
44 type gauge circonusgometrics.Gauge
45
46 // Name implements Gauge.
47 func (g gauge) Name() string {
48 return string(g)
49 }
50
51 // With implements Gauge, but is a no-op.
52 func (g gauge) With(metrics.Field) metrics.Gauge {
53 return g
54 }
55
56 // Set implements Gauge.
57 func (g gauge) Set(value float64) {
58 circonusgometrics.Gauge(g).Set(int64(value))
59 }
60
61 // Add implements Gauge, but is a no-op, as Circonus gauges don't support
62 // incremental (delta) mutation.
63 func (g gauge) Add(float64) {
64 return
65 }
66
67 // Get implements Gauge, but always returns zero, as there's no way to extract
68 // the current value from a Circonus gauge.
69 func (g gauge) Get() float64 {
70 return 0.0
71 }
72
73 // NewHistogram returns a histogram backed by a Circonus histogram.
74 // Due to the Circonus data model, fields are not supported.
75 func NewHistogram(name string) metrics.Histogram {
76 return histogram{
77 h: circonusgometrics.NewHistogram(name),
78 }
79 }
80
81 type histogram struct {
82 h *circonusgometrics.Histogram
83 }
84
85 // Name implements Histogram.
86 func (h histogram) Name() string {
87 return h.h.Name()
88 }
89
90 // With implements Histogram, but is a no-op.
91 func (h histogram) With(metrics.Field) metrics.Histogram {
92 return h
93 }
94
95 // Observe implements Histogram. The value is converted to float64.
96 func (h histogram) Observe(value int64) {
97 h.h.RecordValue(float64(value))
98 }
99
100 // Distribution implements Histogram, but is a no-op.
101 func (h histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) {
102 return []metrics.Bucket{}, []metrics.Quantile{}
103 }
0 package circonus
1
2 import (
3 "encoding/json"
4 "errors"
5 "io/ioutil"
6 "log"
7 "net/http"
8 "net/http/httptest"
9 "sync"
10 "testing"
11 "time"
12
13 "github.com/circonus-labs/circonus-gometrics"
14
15 "github.com/go-kit/kit/metrics"
16 "github.com/go-kit/kit/metrics/teststat"
17 )
18
19 var (
20 // The Circonus Start() method launches a new goroutine that cannot be
21 // stopped. So, make sure we only do that once per test run.
22 onceStart sync.Once
23
24 // Similarly, once set, the submission interval cannot be changed.
25 submissionInterval = 50 * time.Millisecond
26 )
27
28 func TestCounter(t *testing.T) {
29 log.SetOutput(ioutil.Discard) // Circonus logs errors directly! Bad Circonus!
30 defer circonusgometrics.Reset() // Circonus has package global state! Bad Circonus!
31
32 var (
33 name = "test_counter"
34 value uint64
35 )
36 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37 m := map[string]postCounter{}
38 json.NewDecoder(r.Body).Decode(&m)
39 value = m[name].Value
40 }))
41 defer s.Close()
42
43 // We must set the submission URL before making observations. Circonus emits
44 // using a package-global goroutine that is started with Start() but not
45 // stoppable. Once it gets going, it will POST to the package-global
46 // submission URL every interval. If we record observations first, and then
47 // try to change the submission URL, it's possible that the observations
48 // have already been submitted to the previous URL. And, at least in the
49 // case of histograms, every submit, success or failure, resets the data.
50 // Bad Circonus!
51
52 circonusgometrics.WithSubmissionUrl(s.URL)
53 circonusgometrics.WithInterval(submissionInterval)
54
55 c := NewCounter(name)
56
57 if want, have := name, c.Name(); want != have {
58 t.Errorf("want %q, have %q", want, have)
59 }
60
61 c.Add(123)
62 c.With(metrics.Field{Key: "this should", Value: "be ignored"}).Add(456)
63
64 onceStart.Do(func() { circonusgometrics.Start() })
65 if err := within(time.Second, func() bool {
66 return value > 0
67 }); err != nil {
68 t.Fatalf("error collecting results: %v", err)
69 }
70
71 if want, have := 123+456, int(value); want != have {
72 t.Errorf("want %d, have %d", want, have)
73 }
74 }
75
76 func TestGauge(t *testing.T) {
77 log.SetOutput(ioutil.Discard) // Circonus logs errors directly! Bad Circonus!
78 defer circonusgometrics.Reset() // Circonus has package global state! Bad Circonus!
79
80 var (
81 name = "test_gauge"
82 value float64
83 )
84 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85 m := map[string]postGauge{}
86 json.NewDecoder(r.Body).Decode(&m)
87 value = m[name].Value
88 }))
89 defer s.Close()
90
91 circonusgometrics.WithSubmissionUrl(s.URL)
92 circonusgometrics.WithInterval(submissionInterval)
93
94 g := NewGauge(name)
95
96 g.Set(123)
97 g.Add(456) // is a no-op
98
99 if want, have := 0.0, g.Get(); want != have {
100 t.Errorf("Get should always return %.2f, but I got %.2f", want, have)
101 }
102
103 onceStart.Do(func() { circonusgometrics.Start() })
104
105 if err := within(time.Second, func() bool {
106 return value > 0.0
107 }); err != nil {
108 t.Fatalf("error collecting results: %v", err)
109 }
110
111 if want, have := 123.0, value; want != have {
112 t.Errorf("want %.2f, have %.2f", want, have)
113 }
114 }
115
116 func TestHistogram(t *testing.T) {
117 log.SetOutput(ioutil.Discard) // Circonus logs errors directly! Bad Circonus!
118 defer circonusgometrics.Reset() // Circonus has package global state! Bad Circonus!
119
120 var (
121 name = "test_histogram"
122 result []string
123 onceDecode sync.Once
124 )
125 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126 onceDecode.Do(func() {
127 m := map[string]postHistogram{}
128 json.NewDecoder(r.Body).Decode(&m)
129 result = m[name].Value
130 })
131 }))
132 defer s.Close()
133
134 circonusgometrics.WithSubmissionUrl(s.URL)
135 circonusgometrics.WithInterval(submissionInterval)
136
137 h := NewHistogram(name)
138
139 var (
140 seed = int64(123)
141 mean = int64(500)
142 stdev = int64(123)
143 min = int64(0)
144 max = 2 * mean
145 )
146 teststat.PopulateNormalHistogram(t, h, seed, mean, stdev)
147
148 onceStart.Do(func() { circonusgometrics.Start() })
149
150 if err := within(time.Second, func() bool {
151 return len(result) > 0
152 }); err != nil {
153 t.Fatalf("error collecting results: %v", err)
154 }
155
156 teststat.AssertCirconusNormalHistogram(t, mean, stdev, min, max, result)
157 }
158
159 func within(d time.Duration, f func() bool) error {
160 deadline := time.Now().Add(d)
161 for {
162 if time.Now().After(deadline) {
163 return errors.New("deadline exceeded")
164 }
165 if f() {
166 return nil
167 }
168 time.Sleep(d / 10)
169 }
170 }
171
172 // These are reverse-engineered from the POST body.
173
174 type postCounter struct {
175 Value uint64 `json:"_value"`
176 }
177
178 type postGauge struct {
179 Value float64 `json:"_value"`
180 }
181
182 type postHistogram struct {
183 Value []string `json:"_value"`
184 }
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 = 20
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 }