-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #277 from go-kit/circonus
metrics: add Circonus backend
- Loading branch information
Showing
3 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Package circonus provides a Circonus backend for package metrics. | ||
// | ||
// Users are responsible for calling the circonusgometrics.Start method | ||
// themselves. Note that all Circonus metrics must have unique names, and are | ||
// registered in a package-global registry. Circonus metrics also don't support | ||
// fields, so all With methods are no-ops. | ||
package circonus | ||
|
||
import ( | ||
"github.com/circonus-labs/circonus-gometrics" | ||
|
||
"github.com/go-kit/kit/metrics" | ||
) | ||
|
||
// NewCounter returns a counter backed by a Circonus counter with the given | ||
// name. Due to the Circonus data model, fields are not supported. | ||
func NewCounter(name string) metrics.Counter { | ||
return counter(name) | ||
} | ||
|
||
type counter circonusgometrics.Counter | ||
|
||
// Name implements Counter. | ||
func (c counter) Name() string { | ||
return string(c) | ||
} | ||
|
||
// With implements Counter, but is a no-op. | ||
func (c counter) With(metrics.Field) metrics.Counter { | ||
return c | ||
} | ||
|
||
// Add implements Counter. | ||
func (c counter) Add(delta uint64) { | ||
circonusgometrics.Counter(c).AddN(delta) | ||
} | ||
|
||
// NewGauge returns a gauge backed by a Circonus gauge with the given name. Due | ||
// to the Circonus data model, fields are not supported. Also, Circonus gauges | ||
// are defined as integers, so values are truncated. | ||
func NewGauge(name string) metrics.Gauge { | ||
return gauge(name) | ||
} | ||
|
||
type gauge circonusgometrics.Gauge | ||
|
||
// Name implements Gauge. | ||
func (g gauge) Name() string { | ||
return string(g) | ||
} | ||
|
||
// With implements Gauge, but is a no-op. | ||
func (g gauge) With(metrics.Field) metrics.Gauge { | ||
return g | ||
} | ||
|
||
// Set implements Gauge. | ||
func (g gauge) Set(value float64) { | ||
circonusgometrics.Gauge(g).Set(int64(value)) | ||
} | ||
|
||
// Add implements Gauge, but is a no-op, as Circonus gauges don't support | ||
// incremental (delta) mutation. | ||
func (g gauge) Add(float64) { | ||
return | ||
} | ||
|
||
// Get implements Gauge, but always returns zero, as there's no way to extract | ||
// the current value from a Circonus gauge. | ||
func (g gauge) Get() float64 { | ||
return 0.0 | ||
} | ||
|
||
// NewHistogram returns a histogram backed by a Circonus histogram. | ||
// Due to the Circonus data model, fields are not supported. | ||
func NewHistogram(name string) metrics.Histogram { | ||
return histogram{ | ||
h: circonusgometrics.NewHistogram(name), | ||
} | ||
} | ||
|
||
type histogram struct { | ||
h *circonusgometrics.Histogram | ||
} | ||
|
||
// Name implements Histogram. | ||
func (h histogram) Name() string { | ||
return h.h.Name() | ||
} | ||
|
||
// With implements Histogram, but is a no-op. | ||
func (h histogram) With(metrics.Field) metrics.Histogram { | ||
return h | ||
} | ||
|
||
// Observe implements Histogram. The value is converted to float64. | ||
func (h histogram) Observe(value int64) { | ||
h.h.RecordValue(float64(value)) | ||
} | ||
|
||
// Distribution implements Histogram, but is a no-op. | ||
func (h histogram) Distribution() ([]metrics.Bucket, []metrics.Quantile) { | ||
return []metrics.Bucket{}, []metrics.Quantile{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package circonus | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"net/http/httptest" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"github.com/circonus-labs/circonus-gometrics" | ||
|
||
"github.com/go-kit/kit/metrics" | ||
"github.com/go-kit/kit/metrics/teststat" | ||
) | ||
|
||
var ( | ||
// The Circonus Start() method launches a new goroutine that cannot be | ||
// stopped. So, make sure we only do that once per test run. | ||
onceStart sync.Once | ||
|
||
// Similarly, once set, the submission interval cannot be changed. | ||
submissionInterval = 50 * time.Millisecond | ||
) | ||
|
||
func TestCounter(t *testing.T) { | ||
log.SetOutput(ioutil.Discard) // Circonus logs errors directly! Bad Circonus! | ||
defer circonusgometrics.Reset() // Circonus has package global state! Bad Circonus! | ||
|
||
var ( | ||
name = "test_counter" | ||
value uint64 | ||
mtx sync.Mutex | ||
) | ||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
type postCounter struct { | ||
Value uint64 `json:"_value"` // reverse-engineered | ||
} | ||
m := map[string]postCounter{} | ||
json.NewDecoder(r.Body).Decode(&m) | ||
mtx.Lock() | ||
defer mtx.Unlock() | ||
value = m[name].Value | ||
})) | ||
defer s.Close() | ||
|
||
// We must set the submission URL before making observations. Circonus emits | ||
// using a package-global goroutine that is started with Start() but not | ||
// stoppable. Once it gets going, it will POST to the package-global | ||
// submission URL every interval. If we record observations first, and then | ||
// try to change the submission URL, it's possible that the observations | ||
// have already been submitted to the previous URL. And, at least in the | ||
// case of histograms, every submit, success or failure, resets the data. | ||
// Bad Circonus! | ||
|
||
circonusgometrics.WithSubmissionUrl(s.URL) | ||
circonusgometrics.WithInterval(submissionInterval) | ||
|
||
c := NewCounter(name) | ||
|
||
if want, have := name, c.Name(); want != have { | ||
t.Errorf("want %q, have %q", want, have) | ||
} | ||
|
||
c.Add(123) | ||
c.With(metrics.Field{Key: "this should", Value: "be ignored"}).Add(456) | ||
|
||
onceStart.Do(func() { circonusgometrics.Start() }) | ||
if err := within(time.Second, func() bool { | ||
mtx.Lock() | ||
defer mtx.Unlock() | ||
return value > 0 | ||
}); err != nil { | ||
t.Fatalf("error collecting results: %v", err) | ||
} | ||
|
||
if want, have := 123+456, int(value); want != have { | ||
t.Errorf("want %d, have %d", want, have) | ||
} | ||
} | ||
|
||
func TestGauge(t *testing.T) { | ||
log.SetOutput(ioutil.Discard) | ||
defer circonusgometrics.Reset() | ||
|
||
var ( | ||
name = "test_gauge" | ||
value float64 | ||
mtx sync.Mutex | ||
) | ||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
type postGauge struct { | ||
Value float64 `json:"_value"` | ||
} | ||
m := map[string]postGauge{} | ||
json.NewDecoder(r.Body).Decode(&m) | ||
mtx.Lock() | ||
defer mtx.Unlock() | ||
value = m[name].Value | ||
})) | ||
defer s.Close() | ||
|
||
circonusgometrics.WithSubmissionUrl(s.URL) | ||
circonusgometrics.WithInterval(submissionInterval) | ||
|
||
g := NewGauge(name) | ||
|
||
g.Set(123) | ||
g.Add(456) // is a no-op | ||
|
||
if want, have := 0.0, g.Get(); want != have { | ||
t.Errorf("Get should always return %.2f, but I got %.2f", want, have) | ||
} | ||
|
||
onceStart.Do(func() { circonusgometrics.Start() }) | ||
|
||
if err := within(time.Second, func() bool { | ||
mtx.Lock() | ||
defer mtx.Unlock() | ||
return value > 0.0 | ||
}); err != nil { | ||
t.Fatalf("error collecting results: %v", err) | ||
} | ||
|
||
if want, have := 123.0, value; want != have { | ||
t.Errorf("want %.2f, have %.2f", want, have) | ||
} | ||
} | ||
|
||
func TestHistogram(t *testing.T) { | ||
log.SetOutput(ioutil.Discard) | ||
defer circonusgometrics.Reset() | ||
|
||
var ( | ||
name = "test_histogram" | ||
result []string | ||
mtx sync.Mutex | ||
onceDecode sync.Once | ||
) | ||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
type postHistogram struct { | ||
Value []string `json:"_value"` | ||
} | ||
onceDecode.Do(func() { | ||
m := map[string]postHistogram{} | ||
json.NewDecoder(r.Body).Decode(&m) | ||
mtx.Lock() | ||
defer mtx.Unlock() | ||
result = m[name].Value | ||
}) | ||
})) | ||
defer s.Close() | ||
|
||
circonusgometrics.WithSubmissionUrl(s.URL) | ||
circonusgometrics.WithInterval(submissionInterval) | ||
|
||
h := NewHistogram(name) | ||
|
||
var ( | ||
seed = int64(123) | ||
mean = int64(500) | ||
stdev = int64(123) | ||
min = int64(0) | ||
max = 2 * mean | ||
) | ||
teststat.PopulateNormalHistogram(t, h, seed, mean, stdev) | ||
|
||
onceStart.Do(func() { circonusgometrics.Start() }) | ||
|
||
if err := within(time.Second, func() bool { | ||
mtx.Lock() | ||
defer mtx.Unlock() | ||
return len(result) > 0 | ||
}); err != nil { | ||
t.Fatalf("error collecting results: %v", err) | ||
} | ||
|
||
teststat.AssertCirconusNormalHistogram(t, mean, stdev, min, max, result) | ||
} | ||
|
||
func within(d time.Duration, f func() bool) error { | ||
deadline := time.Now().Add(d) | ||
for { | ||
if time.Now().After(deadline) { | ||
return errors.New("deadline exceeded") | ||
} | ||
if f() { | ||
return nil | ||
} | ||
time.Sleep(d / 10) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package teststat | ||
|
||
import ( | ||
"math" | ||
"strconv" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/codahale/hdrhistogram" | ||
) | ||
|
||
// AssertCirconusNormalHistogram ensures the Circonus Histogram data captured in | ||
// the result slice abides a normal distribution. | ||
func AssertCirconusNormalHistogram(t *testing.T, mean, stdev, min, max int64, result []string) { | ||
if len(result) <= 0 { | ||
t.Fatal("no results") | ||
} | ||
|
||
// Circonus just dumps the raw counts. We need to do our own statistical analysis. | ||
h := hdrhistogram.New(min, max, 3) | ||
|
||
for _, s := range result { | ||
// "H[1.23e04]=123" | ||
toks := strings.Split(s, "=") | ||
if len(toks) != 2 { | ||
t.Fatalf("bad H value: %q", s) | ||
} | ||
|
||
var bucket string | ||
bucket = toks[0] | ||
bucket = bucket[2 : len(bucket)-1] // "H[1.23e04]" -> "1.23e04" | ||
f, err := strconv.ParseFloat(bucket, 64) | ||
if err != nil { | ||
t.Fatalf("error parsing H value: %q: %v", s, err) | ||
} | ||
|
||
count, err := strconv.ParseFloat(toks[1], 64) | ||
if err != nil { | ||
t.Fatalf("error parsing H count: %q: %v", s, err) | ||
} | ||
|
||
h.RecordValues(int64(f), int64(count)) | ||
} | ||
|
||
// Apparently Circonus buckets observations by dropping a sigfig, so we have | ||
// very coarse tolerance. | ||
var tolerance int64 = 30 | ||
for _, quantile := range []int{50, 90, 99} { | ||
want := normalValueAtQuantile(mean, stdev, quantile) | ||
have := h.ValueAtQuantile(float64(quantile)) | ||
if int64(math.Abs(float64(want)-float64(have))) > tolerance { | ||
t.Errorf("quantile %d: want %d, have %d", quantile, want, have) | ||
} | ||
} | ||
} |