Moved to google.golang.org/genproto/googleapis/api/annotations

Fixes #52
This commit is contained in:
Valerio Gheri
2017-03-31 18:01:58 +02:00
parent 024c5a4e4e
commit c40779224f
2037 changed files with 831329 additions and 1854 deletions

97
vendor/github.com/go-kit/kit/metrics/README.md generated vendored Normal file
View File

@@ -0,0 +1,97 @@
# package metrics
`package metrics` provides a set of uniform interfaces for service instrumentation.
It has
[counters](http://prometheus.io/docs/concepts/metric_types/#counter),
[gauges](http://prometheus.io/docs/concepts/metric_types/#gauge), and
[histograms](http://prometheus.io/docs/concepts/metric_types/#histogram),
and provides adapters to popular metrics packages, like
[expvar](https://golang.org/pkg/expvar),
[StatsD](https://github.com/etsy/statsd), and
[Prometheus](https://prometheus.io).
## Rationale
Code instrumentation is absolutely essential to achieve
[observability](https://speakerdeck.com/mattheath/observability-in-micro-service-architectures)
into a distributed system.
Metrics and instrumentation tools have coalesced around a few well-defined idioms.
`package metrics` provides a common, minimal interface those idioms for service authors.
## Usage
A simple counter, exported via expvar.
```go
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/expvar"
)
func main() {
var myCount metrics.Counter
myCount = expvar.NewCounter("my_count")
myCount.Add(1)
}
```
A histogram for request duration,
exported via a Prometheus summary with dynamically-computed quantiles.
```go
import (
"time"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/prometheus"
)
func main() {
var dur metrics.Histogram = prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: "myservice",
Subsystem: "api",
Name: "request_duration_seconds",
Help: "Total time spent serving requests.",
}, []string{})
// ...
}
func handleRequest(dur metrics.Histogram) {
defer func(begin time.Time) { dur.Observe(time.Since(begin).Seconds()) }(time.Now())
// handle request
}
```
A gauge for the number of goroutines currently running, exported via StatsD.
```go
import (
"net"
"os"
"runtime"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/statsd"
)
func main() {
statsd := statsd.New("foo_svc.", log.NewNopLogger())
report := time.NewTicker(5 * time.Second)
defer report.Stop()
go statsd.SendLoop(report.C, "tcp", "statsd.internal:8125")
goroutines := statsd.NewGauge("goroutine_count")
go exportGoroutines(goroutines)
// ...
}
func exportGoroutines(g metrics.Gauge) {
for range time.Tick(time.Second) {
g.Set(float64(runtime.NumGoroutine()))
}
}
```
For more information, see [the package documentation](https://godoc.org/github.com/go-kit/kit/metrics).

View File

@@ -0,0 +1,40 @@
// Package discard provides a no-op metrics backend.
package discard
import "github.com/go-kit/kit/metrics"
type counter struct{}
// NewCounter returns a new no-op counter.
func NewCounter() metrics.Counter { return counter{} }
// With implements Counter.
func (c counter) With(labelValues ...string) metrics.Counter { return c }
// Add implements Counter.
func (c counter) Add(delta float64) {}
type gauge struct{}
// NewGauge returns a new no-op gauge.
func NewGauge() metrics.Gauge { return gauge{} }
// With implements Gauge.
func (g gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set implements Gauge.
func (g gauge) Set(value float64) {}
// Add implements metrics.Gauge.
func (g gauge) Add(delta float64) {}
type histogram struct{}
// NewHistogram returns a new no-op histogram.
func NewHistogram() metrics.Histogram { return histogram{} }
// With implements Histogram.
func (h histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe implements histogram.
func (h histogram) Observe(value float64) {}

96
vendor/github.com/go-kit/kit/metrics/doc.go generated vendored Normal file
View File

@@ -0,0 +1,96 @@
// Package metrics provides a framework for application instrumentation. It's
// primarily designed to help you get started with good and robust
// instrumentation, and to help you migrate from a less-capable system like
// Graphite to a more-capable system like Prometheus. If your organization has
// already standardized on an instrumentation system like Prometheus, and has no
// plans to change, it may make sense to use that system's instrumentation
// library directly.
//
// This package provides three core metric abstractions (Counter, Gauge, and
// Histogram) and implementations for almost all common instrumentation
// backends. Each metric has an observation method (Add, Set, or Observe,
// respectively) used to record values, and a With method to "scope" the
// observation by various parameters. For example, you might have a Histogram to
// record request durations, parameterized by the method that's being called.
//
// var requestDuration metrics.Histogram
// // ...
// requestDuration.With("method", "MyMethod").Observe(time.Since(begin))
//
// This allows a single high-level metrics object (requestDuration) to work with
// many code paths somewhat dynamically. The concept of With is fully supported
// in some backends like Prometheus, and not supported in other backends like
// Graphite. So, With may be a no-op, depending on the concrete implementation
// you choose. Please check the implementation to know for sure. For
// implementations that don't provide With, it's necessary to fully parameterize
// each metric in the metric name, e.g.
//
// // Statsd
// c := statsd.NewCounter("request_duration_MyMethod_200")
// c.Add(1)
//
// // Prometheus
// c := prometheus.NewCounter(stdprometheus.CounterOpts{
// Name: "request_duration",
// ...
// }, []string{"method", "status_code"})
// c.With("method", "MyMethod", "status_code", strconv.Itoa(code)).Add(1)
//
// Usage
//
// Metrics are dependencies, and should be passed to the components that need
// them in the same way you'd construct and pass a database handle, or reference
// to another component. Metrics should *not* be created in the global scope.
// Instead, instantiate metrics in your func main, using whichever concrete
// implementation is appropriate for your organization.
//
// latency := prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
// Namespace: "myteam",
// Subsystem: "foosvc",
// Name: "request_latency_seconds",
// Help: "Incoming request latency in seconds."
// }, []string{"method", "status_code"})
//
// Write your components to take the metrics they will use as parameters to
// their constructors. Use the interface types, not the concrete types. That is,
//
// // NewAPI takes metrics.Histogram, not *prometheus.Summary
// func NewAPI(s Store, logger log.Logger, latency metrics.Histogram) *API {
// // ...
// }
//
// func (a *API) ServeFoo(w http.ResponseWriter, r *http.Request) {
// begin := time.Now()
// // ...
// a.latency.Observe(time.Since(begin).Seconds())
// }
//
// Finally, pass the metrics as dependencies when building your object graph.
// This should happen in func main, not in the global scope.
//
// api := NewAPI(store, logger, latency)
// http.ListenAndServe("/", api)
//
// Note that metrics are "write-only" interfaces.
//
// Implementation details
//
// All metrics are safe for concurrent use. Considerable design influence has
// been taken from https://github.com/codahale/metrics and
// https://prometheus.io.
//
// Each telemetry system has different semantics for label values, push vs.
// pull, support for histograms, etc. These properties influence the design of
// their respective packages. This table attempts to summarize the key points of
// distinction.
//
// SYSTEM DIM COUNTERS GAUGES HISTOGRAMS
// dogstatsd n batch, push-aggregate batch, push-aggregate native, batch, push-each
// statsd 1 batch, push-aggregate batch, push-aggregate native, batch, push-each
// graphite 1 batch, push-aggregate batch, push-aggregate synthetic, batch, push-aggregate
// expvar 1 atomic atomic synthetic, batch, in-place expose
// influx n custom custom custom
// prometheus n native native native
// pcp 1 native native native
//
package metrics

View File

@@ -0,0 +1,314 @@
// Package dogstatsd provides a DogStatsD backend for package metrics. It's very
// similar to StatsD, but supports arbitrary tags per-metric, which map to Go
// kit's label values. So, while label values are no-ops in StatsD, they are
// supported here. For more details, see the documentation at
// http://docs.datadoghq.com/guides/dogstatsd/.
//
// This package batches observations and emits them on some schedule to the
// remote server. This is useful even if you connect to your DogStatsD server
// over UDP. Emitting one network packet per observation can quickly overwhelm
// even the fastest internal network.
package dogstatsd
import (
"fmt"
"io"
"strings"
"time"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/kit/metrics/internal/ratemap"
"github.com/go-kit/kit/util/conn"
)
// Dogstatsd receives metrics observations and forwards them to a DogStatsD
// server. Create a Dogstatsd object, use it to create metrics, and pass those
// metrics as dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Timings and
// histograms are buffered but not aggregated.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a DogStatsD server, use the SendLoop helper method.
type Dogstatsd struct {
prefix string
rates *ratemap.RateMap
counters *lv.Space
gauges *lv.Space
timings *lv.Space
histograms *lv.Space
logger log.Logger
}
// New returns a Dogstatsd object that may be used to create metrics. Prefix is
// applied to all created metrics. Callers must ensure that regular calls to
// WriteTo are performed, either manually or with one of the helper methods.
func New(prefix string, logger log.Logger) *Dogstatsd {
return &Dogstatsd{
prefix: prefix,
rates: ratemap.New(),
counters: lv.NewSpace(),
gauges: lv.NewSpace(),
timings: lv.NewSpace(),
histograms: lv.NewSpace(),
logger: logger,
}
}
// NewCounter returns a counter, sending observations to this Dogstatsd object.
func (d *Dogstatsd) NewCounter(name string, sampleRate float64) *Counter {
d.rates.Set(d.prefix+name, sampleRate)
return &Counter{
name: d.prefix + name,
obs: d.counters.Observe,
}
}
// NewGauge returns a gauge, sending observations to this Dogstatsd object.
func (d *Dogstatsd) NewGauge(name string) *Gauge {
return &Gauge{
name: d.prefix + name,
obs: d.gauges.Observe,
add: d.gauges.Add,
}
}
// NewTiming returns a histogram whose observations are interpreted as
// millisecond durations, and are forwarded to this Dogstatsd object.
func (d *Dogstatsd) NewTiming(name string, sampleRate float64) *Timing {
d.rates.Set(d.prefix+name, sampleRate)
return &Timing{
name: d.prefix + name,
obs: d.timings.Observe,
}
}
// NewHistogram returns a histogram whose observations are of an unspecified
// unit, and are forwarded to this Dogstatsd object.
func (d *Dogstatsd) NewHistogram(name string, sampleRate float64) *Histogram {
d.rates.Set(d.prefix+name, sampleRate)
return &Histogram{
name: d.prefix + name,
obs: d.histograms.Observe,
}
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until the channel is
// closed, so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (d *Dogstatsd) WriteLoop(c <-chan time.Time, w io.Writer) {
for range c {
if _, err := d.WriteTo(w); err != nil {
d.logger.Log("during", "WriteTo", "err", err)
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until the channel is closed, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (d *Dogstatsd) SendLoop(c <-chan time.Time, network, address string) {
d.WriteLoop(c, conn.NewDefaultManager(network, address, d.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// DogStatsD format. WriteTo abides best-effort semantics, so observations are
// lost if there is a problem with the write. Clients should be sure to call
// WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
func (d *Dogstatsd) WriteTo(w io.Writer) (count int64, err error) {
var n int
d.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s:%f|c%s%s\n", name, sum(values), sampling(d.rates.Get(name)), tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
d.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s:%f|g%s\n", name, last(values), tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
d.timings.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
sampleRate := d.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s:%f|ms%s%s\n", name, value, sampling(sampleRate), tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
d.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
sampleRate := d.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s:%f|h%s%s\n", name, value, sampling(sampleRate), tagValues(lvs))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
return count, err
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func last(a []float64) float64 {
return a[len(a)-1]
}
func sampling(r float64) string {
var sv string
if r < 1.0 {
sv = fmt.Sprintf("|@%f", r)
}
return sv
}
func tagValues(labelValues []string) string {
if len(labelValues) == 0 {
return ""
}
if len(labelValues)%2 != 0 {
panic("tagValues received a labelValues with an odd number of strings")
}
pairs := make([]string, 0, len(labelValues)/2)
for i := 0; i < len(labelValues); i += 2 {
pairs = append(pairs, labelValues[i]+":"+labelValues[i+1])
}
return "|#" + strings.Join(pairs, ",")
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is a DogStatsD counter. Observations are forwarded to a Dogstatsd
// object, and aggregated (summed) per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
// Gauge is a DogStatsD gauge. Observations are forwarded to a Dogstatsd
// object, and aggregated (the last observation selected) per timeseries.
type Gauge struct {
name string
lvs lv.LabelValues
obs observeFunc
add observeFunc
}
// With implements metrics.Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
name: g.name,
lvs: g.lvs.With(labelValues...),
obs: g.obs,
add: g.add,
}
}
// Set implements metrics.Gauge.
func (g *Gauge) Set(value float64) {
g.obs(g.name, g.lvs, value)
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.add(g.name, g.lvs, delta)
}
// Timing is a DogStatsD timing, or metrics.Histogram. Observations are
// forwarded to a Dogstatsd object, and collected (but not aggregated) per
// timeseries.
type Timing struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Timing.
func (t *Timing) With(labelValues ...string) metrics.Histogram {
return &Timing{
name: t.name,
lvs: t.lvs.With(labelValues...),
obs: t.obs,
}
}
// Observe implements metrics.Histogram. Value is interpreted as milliseconds.
func (t *Timing) Observe(value float64) {
t.obs(t.name, t.lvs, value)
}
// Histogram is a DogStatsD histrogram. Observations are forwarded to a
// Dogstatsd object, and collected (but not aggregated) per timeseries.
type Histogram struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
name: h.name,
lvs: h.lvs.With(labelValues...),
obs: h.obs,
}
}
// Observe implements metrics.Histogram.
func (h *Histogram) Observe(value float64) {
h.obs(h.name, h.lvs, value)
}

View File

@@ -0,0 +1,90 @@
package dogstatsd
import (
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value"
regex := `^` + prefix + name + `:([0-9\.]+)\|c\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
counter := d.NewCounter(name, 1.0).With(label, value)
valuef := teststat.SumLines(d, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestCounterSampled(t *testing.T) {
// This will involve multiplying the observed sum by the inverse of the
// sample rate and checking against the expected value within some
// tolerance.
t.Skip("TODO")
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc"
regex := `^` + prefix + name + `:([0-9\.]+)\|g\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
gauge := d.NewGauge(name).With(label, value)
valuef := teststat.LastLine(d, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
// DogStatsD histograms just emit all observations. So, we collect them into
// a generic histogram, and run the statistics test on that.
func TestHistogram(t *testing.T) {
prefix, name := "dogstatsd.", "histogram_test"
label, value := "abc", "def"
regex := `^` + prefix + name + `:([0-9\.]+)\|h\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewHistogram(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogramSampled(t *testing.T) {
prefix, name := "dogstatsd.", "sampled_histogram_test"
label, value := "foo", "bar"
regex := `^` + prefix + name + `:([0-9\.]+)\|h\|@0\.01[0]*\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewHistogram(name, 0.01).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50)
if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}
func TestTiming(t *testing.T) {
prefix, name := "dogstatsd.", "timing_test"
label, value := "wiggle", "bottom"
regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewTiming(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50) // no |@0.X
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestTimingSampled(t *testing.T) {
prefix, name := "dogstatsd.", "sampled_timing_test"
label, value := "internal", "external"
regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0.03[0]*\|#` + label + `:` + value + `$`
d := New(prefix, log.NewNopLogger())
histogram := d.NewTiming(name, 0.03).With(label, value)
quantiles := teststat.Quantiles(d, regex, 50)
if err := teststat.TestHistogram(histogram, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}

94
vendor/github.com/go-kit/kit/metrics/expvar/expvar.go generated vendored Normal file
View File

@@ -0,0 +1,94 @@
// Package expvar provides expvar backends for metrics.
// Label values are not supported.
package expvar
import (
"expvar"
"sync"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
)
// Counter implements the counter metric with an expvar float.
// Label values are not supported.
type Counter struct {
f *expvar.Float
}
// NewCounter creates an expvar Float with the given name, and returns an object
// that implements the Counter interface.
func NewCounter(name string) *Counter {
return &Counter{
f: expvar.NewFloat(name),
}
}
// With is a no-op.
func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
// Add implements Counter.
func (c *Counter) Add(delta float64) { c.f.Add(delta) }
// Gauge implements the gauge metric with an expvar float.
// Label values are not supported.
type Gauge struct {
f *expvar.Float
}
// NewGauge creates an expvar Float with the given name, and returns an object
// that implements the Gauge interface.
func NewGauge(name string) *Gauge {
return &Gauge{
f: expvar.NewFloat(name),
}
}
// With is a no-op.
func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set implements Gauge.
func (g *Gauge) Set(value float64) { g.f.Set(value) }
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) { g.f.Add(delta) }
// Histogram implements the histogram metric with a combination of the generic
// Histogram object and several expvar Floats, one for each of the 50th, 90th,
// 95th, and 99th quantiles of observed values, with the quantile attached to
// the name as a suffix. Label values are not supported.
type Histogram struct {
mtx sync.Mutex
h *generic.Histogram
p50 *expvar.Float
p90 *expvar.Float
p95 *expvar.Float
p99 *expvar.Float
}
// NewHistogram returns a Histogram object with the given name and number of
// buckets in the underlying histogram object. 50 is a good default number of
// buckets.
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{
h: generic.NewHistogram(name, buckets),
p50: expvar.NewFloat(name + ".p50"),
p90: expvar.NewFloat(name + ".p90"),
p95: expvar.NewFloat(name + ".p95"),
p99: expvar.NewFloat(name + ".p99"),
}
}
// With is a no-op.
func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe implements Histogram.
func (h *Histogram) Observe(value float64) {
h.mtx.Lock()
defer h.mtx.Unlock()
h.h.Observe(value)
h.p50.Set(h.h.Quantile(0.50))
h.p90.Set(h.h.Quantile(0.90))
h.p95.Set(h.h.Quantile(0.95))
h.p99.Set(h.h.Quantile(0.99))
}

View File

@@ -0,0 +1,38 @@
package expvar
import (
"strconv"
"testing"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
counter := NewCounter("expvar_counter").With("label values", "not supported").(*Counter)
value := func() float64 { f, _ := strconv.ParseFloat(counter.f.String(), 64); return f }
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
gauge := NewGauge("expvar_gauge").With("label values", "not supported").(*Gauge)
value := func() float64 { f, _ := strconv.ParseFloat(gauge.f.String(), 64); return f }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
histogram := NewHistogram("expvar_histogram", 50).With("label values", "not supported").(*Histogram)
quantiles := func() (float64, float64, float64, float64) {
p50, _ := strconv.ParseFloat(histogram.p50.String(), 64)
p90, _ := strconv.ParseFloat(histogram.p90.String(), 64)
p95, _ := strconv.ParseFloat(histogram.p95.String(), 64)
p99, _ := strconv.ParseFloat(histogram.p99.String(), 64)
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}

247
vendor/github.com/go-kit/kit/metrics/generic/generic.go generated vendored Normal file
View File

@@ -0,0 +1,247 @@
// Package generic implements generic versions of each of the metric types. They
// can be embedded by other implementations, and converted to specific formats
// as necessary.
package generic
import (
"fmt"
"io"
"math"
"sync"
"sync/atomic"
"github.com/VividCortex/gohistogram"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
)
// Counter is an in-memory implementation of a Counter.
type Counter struct {
Name string
lvs lv.LabelValues
bits uint64
}
// NewCounter returns a new, usable Counter.
func NewCounter(name string) *Counter {
return &Counter{
Name: name,
}
}
// With implements Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
Name: c.Name,
bits: atomic.LoadUint64(&c.bits),
lvs: c.lvs.With(labelValues...),
}
}
// Add implements Counter.
func (c *Counter) Add(delta float64) {
for {
var (
old = atomic.LoadUint64(&c.bits)
newf = math.Float64frombits(old) + delta
new = math.Float64bits(newf)
)
if atomic.CompareAndSwapUint64(&c.bits, old, new) {
break
}
}
}
// Value returns the current value of the counter.
func (c *Counter) Value() float64 {
return math.Float64frombits(atomic.LoadUint64(&c.bits))
}
// ValueReset returns the current value of the counter, and resets it to zero.
// This is useful for metrics backends whose counter aggregations expect deltas,
// like Graphite.
func (c *Counter) ValueReset() float64 {
for {
var (
old = atomic.LoadUint64(&c.bits)
newf = 0.0
new = math.Float64bits(newf)
)
if atomic.CompareAndSwapUint64(&c.bits, old, new) {
return math.Float64frombits(old)
}
}
}
// LabelValues returns the set of label values attached to the counter.
func (c *Counter) LabelValues() []string {
return c.lvs
}
// Gauge is an in-memory implementation of a Gauge.
type Gauge struct {
Name string
lvs lv.LabelValues
bits uint64
}
// NewGauge returns a new, usable Gauge.
func NewGauge(name string) *Gauge {
return &Gauge{
Name: name,
}
}
// With implements Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
Name: g.Name,
bits: atomic.LoadUint64(&g.bits),
lvs: g.lvs.With(labelValues...),
}
}
// Set implements Gauge.
func (g *Gauge) Set(value float64) {
atomic.StoreUint64(&g.bits, math.Float64bits(value))
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
for {
var (
old = atomic.LoadUint64(&g.bits)
newf = math.Float64frombits(old) + delta
new = math.Float64bits(newf)
)
if atomic.CompareAndSwapUint64(&g.bits, old, new) {
break
}
}
}
// Value returns the current value of the gauge.
func (g *Gauge) Value() float64 {
return math.Float64frombits(atomic.LoadUint64(&g.bits))
}
// LabelValues returns the set of label values attached to the gauge.
func (g *Gauge) LabelValues() []string {
return g.lvs
}
// Histogram is an in-memory implementation of a streaming histogram, based on
// VividCortex/gohistogram. It dynamically computes quantiles, so it's not
// suitable for aggregation.
type Histogram struct {
Name string
lvs lv.LabelValues
h *safeHistogram
}
// NewHistogram returns a numeric histogram based on VividCortex/gohistogram. A
// good default value for buckets is 50.
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{
Name: name,
h: &safeHistogram{Histogram: gohistogram.NewHistogram(buckets)},
}
}
// With implements Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
Name: h.Name,
lvs: h.lvs.With(labelValues...),
h: h.h,
}
}
// Observe implements Histogram.
func (h *Histogram) Observe(value float64) {
h.h.Lock()
defer h.h.Unlock()
h.h.Add(value)
}
// Quantile returns the value of the quantile q, 0.0 < q < 1.0.
func (h *Histogram) Quantile(q float64) float64 {
h.h.RLock()
defer h.h.RUnlock()
return h.h.Quantile(q)
}
// LabelValues returns the set of label values attached to the histogram.
func (h *Histogram) LabelValues() []string {
return h.lvs
}
// Print writes a string representation of the histogram to the passed writer.
// Useful for printing to a terminal.
func (h *Histogram) Print(w io.Writer) {
h.h.RLock()
defer h.h.RUnlock()
fmt.Fprintf(w, h.h.String())
}
// safeHistogram exists as gohistogram.Histogram is not goroutine-safe.
type safeHistogram struct {
sync.RWMutex
gohistogram.Histogram
}
// Bucket is a range in a histogram which aggregates observations.
type Bucket struct {
From, To, Count int64
}
// Quantile is a pair of a quantile (0..100) and its observed maximum value.
type Quantile struct {
Quantile int // 0..100
Value int64
}
// SimpleHistogram is an in-memory implementation of a Histogram. It only tracks
// an approximate moving average, so is likely too naïve for many use cases.
type SimpleHistogram struct {
mtx sync.RWMutex
lvs lv.LabelValues
avg float64
n uint64
}
// NewSimpleHistogram returns a SimpleHistogram, ready for observations.
func NewSimpleHistogram() *SimpleHistogram {
return &SimpleHistogram{}
}
// With implements Histogram.
func (h *SimpleHistogram) With(labelValues ...string) metrics.Histogram {
return &SimpleHistogram{
lvs: h.lvs.With(labelValues...),
avg: h.avg,
n: h.n,
}
}
// Observe implements Histogram.
func (h *SimpleHistogram) Observe(value float64) {
h.mtx.Lock()
defer h.mtx.Unlock()
h.n++
h.avg -= h.avg / float64(h.n)
h.avg += value / float64(h.n)
}
// ApproximateMovingAverage returns the approximate moving average of observations.
func (h *SimpleHistogram) ApproximateMovingAverage() float64 {
h.mtx.RLock()
defer h.mtx.RUnlock()
return h.avg
}
// LabelValues returns the set of label values attached to the histogram.
func (h *SimpleHistogram) LabelValues() []string {
return h.lvs
}

View File

@@ -0,0 +1,109 @@
package generic_test
// This is package generic_test in order to get around an import cycle: this
// package imports teststat to do its testing, but package teststat imports
// generic to use its Histogram in the Quantiles helper function.
import (
"math"
"math/rand"
"sync"
"testing"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
name := "my_counter"
counter := generic.NewCounter(name).With("label", "counter").(*generic.Counter)
if want, have := name, counter.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
value := func() float64 { return counter.Value() }
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestValueReset(t *testing.T) {
counter := generic.NewCounter("test_value_reset")
counter.Add(123)
counter.Add(456)
counter.Add(789)
if want, have := float64(123+456+789), counter.ValueReset(); want != have {
t.Errorf("want %f, have %f", want, have)
}
if want, have := float64(0), counter.Value(); want != have {
t.Errorf("want %f, have %f", want, have)
}
}
func TestGauge(t *testing.T) {
name := "my_gauge"
gauge := generic.NewGauge(name).With("label", "gauge").(*generic.Gauge)
if want, have := name, gauge.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
value := func() float64 { return gauge.Value() }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
name := "my_histogram"
histogram := generic.NewHistogram(name, 50).With("label", "histogram").(*generic.Histogram)
if want, have := name, histogram.Name; want != have {
t.Errorf("Name: want %q, have %q", want, have)
}
quantiles := func() (float64, float64, float64, float64) {
return histogram.Quantile(0.50), histogram.Quantile(0.90), histogram.Quantile(0.95), histogram.Quantile(0.99)
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestIssue424(t *testing.T) {
var (
histogram = generic.NewHistogram("dont_panic", 50)
concurrency = 100
operations = 1000
wg sync.WaitGroup
)
wg.Add(concurrency)
for i := 0; i < concurrency; i++ {
go func() {
defer wg.Done()
for j := 0; j < operations; j++ {
histogram.Observe(float64(j))
histogram.Observe(histogram.Quantile(0.5))
}
}()
}
wg.Wait()
}
func TestSimpleHistogram(t *testing.T) {
histogram := generic.NewSimpleHistogram().With("label", "simple_histogram").(*generic.SimpleHistogram)
var (
sum int
count = 1234 // not too big
)
for i := 0; i < count; i++ {
value := rand.Intn(1000)
sum += value
histogram.Observe(float64(value))
}
var (
want = float64(sum) / float64(count)
have = histogram.ApproximateMovingAverage()
tolerance = 0.001 // real real slim
)
if math.Abs(want-have)/want > tolerance {
t.Errorf("want %f, have %f", want, have)
}
}

View File

@@ -0,0 +1,203 @@
// Package graphite provides a Graphite backend for metrics. Metrics are batched
// and emitted in the plaintext protocol. For more information, see
// http://graphite.readthedocs.io/en/latest/feeding-carbon.html#the-plaintext-protocol
//
// Graphite does not have a native understanding of metric parameterization, so
// label values not supported. Use distinct metrics for each unique combination
// of label values.
package graphite
import (
"fmt"
"io"
"sync"
"time"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/util/conn"
)
// Graphite receives metrics observations and forwards them to a Graphite server.
// Create a Graphite object, use it to create metrics, and pass those metrics as
// dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Histograms are
// exploded into per-quantile gauges and reported once per write.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a Graphite server, use the SendLoop helper method.
type Graphite struct {
mtx sync.RWMutex
prefix string
counters map[string]*Counter
gauges map[string]*Gauge
histograms map[string]*Histogram
logger log.Logger
}
// New returns a Graphite object that may be used to create metrics. Prefix is
// applied to all created metrics. Callers must ensure that regular calls to
// WriteTo are performed, either manually or with one of the helper methods.
func New(prefix string, logger log.Logger) *Graphite {
return &Graphite{
prefix: prefix,
counters: map[string]*Counter{},
gauges: map[string]*Gauge{},
histograms: map[string]*Histogram{},
logger: logger,
}
}
// NewCounter returns a counter. Observations are aggregated and emitted once
// per write invocation.
func (g *Graphite) NewCounter(name string) *Counter {
c := NewCounter(g.prefix + name)
g.mtx.Lock()
g.counters[g.prefix+name] = c
g.mtx.Unlock()
return c
}
// NewGauge returns a gauge. Observations are aggregated and emitted once per
// write invocation.
func (g *Graphite) NewGauge(name string) *Gauge {
ga := NewGauge(g.prefix + name)
g.mtx.Lock()
g.gauges[g.prefix+name] = ga
g.mtx.Unlock()
return ga
}
// NewHistogram returns a histogram. Observations are aggregated and emitted as
// per-quantile gauges, once per write invocation. 50 is a good default value
// for buckets.
func (g *Graphite) NewHistogram(name string, buckets int) *Histogram {
h := NewHistogram(g.prefix+name, buckets)
g.mtx.Lock()
g.histograms[g.prefix+name] = h
g.mtx.Unlock()
return h
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until the channel is
// closed, so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (g *Graphite) WriteLoop(c <-chan time.Time, w io.Writer) {
for range c {
if _, err := g.WriteTo(w); err != nil {
g.logger.Log("during", "WriteTo", "err", err)
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until the channel is closed, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (g *Graphite) SendLoop(c <-chan time.Time, network, address string) {
g.WriteLoop(c, conn.NewDefaultManager(network, address, g.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// Graphite plaintext format. WriteTo abides best-effort semantics, so
// observations are lost if there is a problem with the write. Clients should be
// sure to call WriteTo regularly, ideally through the WriteLoop or SendLoop
// helper methods.
func (g *Graphite) WriteTo(w io.Writer) (count int64, err error) {
g.mtx.RLock()
defer g.mtx.RUnlock()
now := time.Now().Unix()
for name, c := range g.counters {
n, err := fmt.Fprintf(w, "%s %f %d\n", name, c.c.ValueReset(), now)
if err != nil {
return count, err
}
count += int64(n)
}
for name, ga := range g.gauges {
n, err := fmt.Fprintf(w, "%s %f %d\n", name, ga.g.Value(), now)
if err != nil {
return count, err
}
count += int64(n)
}
for name, h := range g.histograms {
for _, p := range []struct {
s string
f float64
}{
{"50", 0.50},
{"90", 0.90},
{"95", 0.95},
{"99", 0.99},
} {
n, err := fmt.Fprintf(w, "%s.p%s %f %d\n", name, p.s, h.h.Quantile(p.f), now)
if err != nil {
return count, err
}
count += int64(n)
}
}
return count, err
}
// Counter is a Graphite counter metric.
type Counter struct {
c *generic.Counter
}
// NewCounter returns a new usable counter metric.
func NewCounter(name string) *Counter {
return &Counter{generic.NewCounter(name)}
}
// With is a no-op.
func (c *Counter) With(...string) metrics.Counter { return c }
// Add implements counter.
func (c *Counter) Add(delta float64) { c.c.Add(delta) }
// Gauge is a Graphite gauge metric.
type Gauge struct {
g *generic.Gauge
}
// NewGauge returns a new usable Gauge metric.
func NewGauge(name string) *Gauge {
return &Gauge{generic.NewGauge(name)}
}
// With is a no-op.
func (g *Gauge) With(...string) metrics.Gauge { return g }
// Set implements gauge.
func (g *Gauge) Set(value float64) { g.g.Set(value) }
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) { g.g.Add(delta) }
// Histogram is a Graphite histogram metric. Observations are bucketed into
// per-quantile gauges.
type Histogram struct {
h *generic.Histogram
}
// NewHistogram returns a new usable Histogram metric.
func NewHistogram(name string, buckets int) *Histogram {
return &Histogram{generic.NewHistogram(name, buckets)}
}
// With is a no-op.
func (h *Histogram) With(...string) metrics.Histogram { return h }
// Observe implements histogram.
func (h *Histogram) Observe(value float64) { h.h.Observe(value) }

View File

@@ -0,0 +1,63 @@
package graphite
import (
"bytes"
"regexp"
"strconv"
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value" // ignored for Graphite
regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
g := New(prefix, log.NewNopLogger())
counter := g.NewCounter(name).With(label, value)
valuef := teststat.SumLines(g, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc" // ignored for Graphite
regex := `^` + prefix + name + ` ([0-9\.]+) [0-9]+$`
g := New(prefix, log.NewNopLogger())
gauge := g.NewGauge(name).With(label, value)
valuef := teststat.LastLine(g, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
// The histogram test is actually like 4 gauge tests.
prefix, name := "graphite.", "histogram_test"
label, value := "abc", "def" // ignored for Graphite
re50 := regexp.MustCompile(prefix + name + `.p50 ([0-9\.]+) [0-9]+`)
re90 := regexp.MustCompile(prefix + name + `.p90 ([0-9\.]+) [0-9]+`)
re95 := regexp.MustCompile(prefix + name + `.p95 ([0-9\.]+) [0-9]+`)
re99 := regexp.MustCompile(prefix + name + `.p99 ([0-9\.]+) [0-9]+`)
g := New(prefix, log.NewNopLogger())
histogram := g.NewHistogram(name, 50).With(label, value)
quantiles := func() (float64, float64, float64, float64) {
var buf bytes.Buffer
g.WriteTo(&buf)
match50 := re50.FindStringSubmatch(buf.String())
p50, _ := strconv.ParseFloat(match50[1], 64)
match90 := re90.FindStringSubmatch(buf.String())
p90, _ := strconv.ParseFloat(match90[1], 64)
match95 := re95.FindStringSubmatch(buf.String())
p95, _ := strconv.ParseFloat(match95[1], 64)
match99 := re99.FindStringSubmatch(buf.String())
p99, _ := strconv.ParseFloat(match99[1], 64)
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,108 @@
package influx
import (
"fmt"
"regexp"
influxdb "github.com/influxdata/influxdb/client/v2"
"github.com/go-kit/kit/log"
)
func ExampleCounter() {
in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
counter := in.NewCounter("influx_counter")
counter.Add(10)
counter.With("error", "true").Add(1)
counter.With("error", "false").Add(2)
counter.Add(50)
client := &bufWriter{}
in.WriteTo(client)
expectedLines := []string{
`(influx_counter,a=b count=60) [0-9]{19}`,
`(influx_counter,a=b,error=true count=1) [0-9]{19}`,
`(influx_counter,a=b,error=false count=2) [0-9]{19}`,
}
if err := extractAndPrintMessage(expectedLines, client.buf.String()); err != nil {
fmt.Println(err.Error())
}
// Output:
// influx_counter,a=b count=60
// influx_counter,a=b,error=true count=1
// influx_counter,a=b,error=false count=2
}
func ExampleGauge() {
in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
gauge := in.NewGauge("influx_gauge")
gauge.Set(10)
gauge.With("error", "true").Set(2)
gauge.With("error", "true").Set(1)
gauge.With("error", "false").Set(2)
gauge.Set(50)
gauge.With("test", "true").Set(1)
gauge.With("test", "true").Add(1)
client := &bufWriter{}
in.WriteTo(client)
expectedLines := []string{
`(influx_gauge,a=b,test=true value=2) [0-9]{19}`,
`(influx_gauge,a=b value=50) [0-9]{19}`,
`(influx_gauge,a=b,error=true value=1) [0-9]{19}`,
`(influx_gauge,a=b,error=false value=2) [0-9]{19}`,
}
if err := extractAndPrintMessage(expectedLines, client.buf.String()); err != nil {
fmt.Println(err.Error())
}
// Output:
// influx_gauge,a=b,test=true value=2
// influx_gauge,a=b value=50
// influx_gauge,a=b,error=true value=1
// influx_gauge,a=b,error=false value=2
}
func ExampleHistogram() {
in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
histogram := in.NewHistogram("influx_histogram")
histogram.Observe(float64(10))
histogram.With("error", "true").Observe(float64(1))
histogram.With("error", "false").Observe(float64(2))
histogram.Observe(float64(50))
client := &bufWriter{}
in.WriteTo(client)
expectedLines := []string{
`(influx_histogram,foo=alpha p50=10,p90=50,p95=50,p99=50) [0-9]{19}`,
`(influx_histogram,error=true,foo=alpha p50=1,p90=1,p95=1,p99=1) [0-9]{19}`,
`(influx_histogram,error=false,foo=alpha p50=2,p90=2,p95=2,p99=2) [0-9]{19}`,
}
if err := extractAndPrintMessage(expectedLines, client.buf.String()); err != nil {
fmt.Println(err.Error())
}
// Output:
// influx_histogram,foo=alpha p50=10,p90=50,p95=50,p99=50
// influx_histogram,error=true,foo=alpha p50=1,p90=1,p95=1,p99=1
// influx_histogram,error=false,foo=alpha p50=2,p90=2,p95=2,p99=2
}
func extractAndPrintMessage(expected []string, msg string) error {
for _, pattern := range expected {
re := regexp.MustCompile(pattern)
match := re.FindStringSubmatch(msg)
if len(match) != 2 {
return fmt.Errorf("Pattern not found! {%s} [%s]: %v\n", pattern, msg, match)
}
fmt.Println(match[1])
}
return nil
}

267
vendor/github.com/go-kit/kit/metrics/influx/influx.go generated vendored Normal file
View File

@@ -0,0 +1,267 @@
// Package influx provides an InfluxDB implementation for metrics. The model is
// similar to other push-based instrumentation systems. Observations are
// aggregated locally and emitted to the Influx server on regular intervals.
package influx
import (
"time"
influxdb "github.com/influxdata/influxdb/client/v2"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/go-kit/kit/metrics/internal/lv"
)
// Influx is a store for metrics that will be emitted to an Influx database.
//
// Influx is a general purpose time-series database, and has no native concepts
// of counters, gauges, or histograms. Counters are modeled as a timeseries with
// one data point per flush, with a "count" field that reflects all adds since
// the last flush. Gauges are modeled as a timeseries with one data point per
// flush, with a "value" field that reflects the current state of the gauge.
// Histograms are modeled as a timeseries with one data point per combination of tags,
// with a set of quantile fields that reflects the p50, p90, p95 & p99.
//
// Influx tags are attached to the Influx object, can be given to each
// metric at construction and can be updated anytime via With function. Influx fields
// are mapped to Go kit label values directly by this collector. Actual metric
// values are provided as fields with specific names depending on the metric.
//
// All observations are collected in memory locally, and flushed on demand.
type Influx struct {
counters *lv.Space
gauges *lv.Space
histograms *lv.Space
tags map[string]string
conf influxdb.BatchPointsConfig
logger log.Logger
}
// New returns an Influx, ready to create metrics and collect observations. Tags
// are applied to all metrics created from this object. The BatchPointsConfig is
// used during flushing.
func New(tags map[string]string, conf influxdb.BatchPointsConfig, logger log.Logger) *Influx {
return &Influx{
counters: lv.NewSpace(),
gauges: lv.NewSpace(),
histograms: lv.NewSpace(),
tags: tags,
conf: conf,
logger: logger,
}
}
// NewCounter returns an Influx counter.
func (in *Influx) NewCounter(name string) *Counter {
return &Counter{
name: name,
obs: in.counters.Observe,
}
}
// NewGauge returns an Influx gauge.
func (in *Influx) NewGauge(name string) *Gauge {
return &Gauge{
name: name,
obs: in.gauges.Observe,
add: in.gauges.Add,
}
}
// NewHistogram returns an Influx histogram.
func (in *Influx) NewHistogram(name string) *Histogram {
return &Histogram{
name: name,
obs: in.histograms.Observe,
}
}
// BatchPointsWriter captures a subset of the influxdb.Client methods necessary
// for emitting metrics observations.
type BatchPointsWriter interface {
Write(influxdb.BatchPoints) error
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until the channel is
// closed, so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (in *Influx) WriteLoop(c <-chan time.Time, w BatchPointsWriter) {
for range c {
if err := in.WriteTo(w); err != nil {
in.logger.Log("during", "WriteTo", "err", err)
}
}
}
// WriteTo flushes the buffered content of the metrics to the writer, in an
// Influx BatchPoints format. WriteTo abides best-effort semantics, so
// observations are lost if there is a problem with the write. Clients should be
// sure to call WriteTo regularly, ideally through the WriteLoop helper method.
func (in *Influx) WriteTo(w BatchPointsWriter) (err error) {
bp, err := influxdb.NewBatchPoints(in.conf)
if err != nil {
return err
}
now := time.Now()
in.counters.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
tags := mergeTags(in.tags, lvs)
var p *influxdb.Point
fields := map[string]interface{}{"count": sum(values)}
p, err = influxdb.NewPoint(name, tags, fields, now)
if err != nil {
return false
}
bp.AddPoint(p)
return true
})
if err != nil {
return err
}
in.gauges.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
tags := mergeTags(in.tags, lvs)
var p *influxdb.Point
fields := map[string]interface{}{"value": last(values)}
p, err = influxdb.NewPoint(name, tags, fields, now)
if err != nil {
return false
}
bp.AddPoint(p)
return true
})
if err != nil {
return err
}
in.histograms.Reset().Walk(func(name string, lvs lv.LabelValues, values []float64) bool {
histogram := generic.NewHistogram(name, 50)
tags := mergeTags(in.tags, lvs)
var p *influxdb.Point
for _, v := range values {
histogram.Observe(v)
}
fields := map[string]interface{}{
"p50": histogram.Quantile(0.50),
"p90": histogram.Quantile(0.90),
"p95": histogram.Quantile(0.95),
"p99": histogram.Quantile(0.99),
}
p, err = influxdb.NewPoint(name, tags, fields, now)
if err != nil {
return false
}
bp.AddPoint(p)
return true
})
if err != nil {
return err
}
return w.Write(bp)
}
func mergeTags(tags map[string]string, labelValues []string) map[string]string {
if len(labelValues)%2 != 0 {
panic("mergeTags received a labelValues with an odd number of strings")
}
ret := make(map[string]string, len(tags)+len(labelValues)/2)
for k, v := range tags {
ret[k] = v
}
for i := 0; i < len(labelValues); i += 2 {
ret[labelValues[i]] = labelValues[i+1]
}
return ret
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func last(a []float64) float64 {
return a[len(a)-1]
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is an Influx counter. Observations are forwarded to an Influx
// object, and aggregated (summed) per timeseries.
type Counter struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
name: c.name,
lvs: c.lvs.With(labelValues...),
obs: c.obs,
}
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, c.lvs, delta)
}
// Gauge is an Influx gauge. Observations are forwarded to a Dogstatsd
// object, and aggregated (the last observation selected) per timeseries.
type Gauge struct {
name string
lvs lv.LabelValues
obs observeFunc
add observeFunc
}
// With implements metrics.Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
name: g.name,
lvs: g.lvs.With(labelValues...),
obs: g.obs,
add: g.add,
}
}
// Set implements metrics.Gauge.
func (g *Gauge) Set(value float64) {
g.obs(g.name, g.lvs, value)
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.add(g.name, g.lvs, delta)
}
// Histogram is an Influx histrogram. Observations are aggregated into a
// generic.Histogram and emitted as per-quantile gauges to the Influx server.
type Histogram struct {
name string
lvs lv.LabelValues
obs observeFunc
}
// With implements metrics.Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
name: h.name,
lvs: h.lvs.With(labelValues...),
obs: h.obs,
}
}
// Observe implements metrics.Histogram.
func (h *Histogram) Observe(value float64) {
h.obs(h.name, h.lvs, value)
}

View File

@@ -0,0 +1,125 @@
package influx
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"testing"
influxdb "github.com/influxdata/influxdb/client/v2"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
in := New(map[string]string{"a": "b"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
re := regexp.MustCompile(`influx_counter,a=b count=([0-9\.]+) [0-9]+`) // reverse-engineered :\
counter := in.NewCounter("influx_counter")
value := func() float64 {
client := &bufWriter{}
in.WriteTo(client)
match := re.FindStringSubmatch(client.buf.String())
f, _ := strconv.ParseFloat(match[1], 64)
return f
}
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
re := regexp.MustCompile(`influx_gauge,foo=alpha value=([0-9\.]+) [0-9]+`)
gauge := in.NewGauge("influx_gauge")
value := func() float64 {
client := &bufWriter{}
in.WriteTo(client)
match := re.FindStringSubmatch(client.buf.String())
f, _ := strconv.ParseFloat(match[1], 64)
return f
}
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
in := New(map[string]string{"foo": "alpha"}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
re := regexp.MustCompile(`influx_histogram,bar=beta,foo=alpha p50=([0-9\.]+),p90=([0-9\.]+),p95=([0-9\.]+),p99=([0-9\.]+) [0-9]+`)
histogram := in.NewHistogram("influx_histogram").With("bar", "beta")
quantiles := func() (float64, float64, float64, float64) {
w := &bufWriter{}
in.WriteTo(w)
match := re.FindStringSubmatch(w.buf.String())
if len(match) != 5 {
t.Errorf("These are not the quantiles you're looking for: %v\n", match)
}
var result [4]float64
for i, q := range match[1:] {
result[i], _ = strconv.ParseFloat(q, 64)
}
return result[0], result[1], result[2], result[3]
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogramLabels(t *testing.T) {
in := New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
h := in.NewHistogram("foo")
h.Observe(123)
h.With("abc", "xyz").Observe(456)
w := &bufWriter{}
if err := in.WriteTo(w); err != nil {
t.Fatal(err)
}
if want, have := 2, len(strings.Split(strings.TrimSpace(w.buf.String()), "\n")); want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestIssue404(t *testing.T) {
in := New(map[string]string{}, influxdb.BatchPointsConfig{}, log.NewNopLogger())
counterOne := in.NewCounter("influx_counter_one").With("a", "b")
counterOne.Add(123)
counterTwo := in.NewCounter("influx_counter_two").With("c", "d")
counterTwo.Add(456)
w := &bufWriter{}
in.WriteTo(w)
lines := strings.Split(strings.TrimSpace(w.buf.String()), "\n")
if want, have := 2, len(lines); want != have {
t.Fatalf("want %d, have %d", want, have)
}
for _, line := range lines {
if strings.HasPrefix(line, "influx_counter_one") {
if !strings.HasPrefix(line, "influx_counter_one,a=b count=123 ") {
t.Errorf("invalid influx_counter_one: %s", line)
}
} else if strings.HasPrefix(line, "influx_counter_two") {
if !strings.HasPrefix(line, "influx_counter_two,c=d count=456 ") {
t.Errorf("invalid influx_counter_two: %s", line)
}
} else {
t.Errorf("unexpected line: %s", line)
}
}
}
type bufWriter struct {
buf bytes.Buffer
}
func (w *bufWriter) Write(bp influxdb.BatchPoints) error {
for _, p := range bp.Points() {
fmt.Fprintf(&w.buf, p.String()+"\n")
}
return nil
}

View File

@@ -0,0 +1,14 @@
package lv
// LabelValues is a type alias that provides validation on its With method.
// Metrics may include it as a member to help them satisfy With semantics and
// save some code duplication.
type LabelValues []string
// With validates the input, and returns a new aggregate labelValues.
func (lvs LabelValues) With(labelValues ...string) LabelValues {
if len(labelValues)%2 != 0 {
labelValues = append(labelValues, "unknown")
}
return append(lvs, labelValues...)
}

View File

@@ -0,0 +1,22 @@
package lv
import (
"strings"
"testing"
)
func TestWith(t *testing.T) {
var a LabelValues
b := a.With("a", "1")
c := a.With("b", "2", "c", "3")
if want, have := "", strings.Join(a, ""); want != have {
t.Errorf("With appears to mutate the original LabelValues: want %q, have %q", want, have)
}
if want, have := "a1", strings.Join(b, ""); want != have {
t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
}
if want, have := "b2c3", strings.Join(c, ""); want != have {
t.Errorf("With does not appear to return the right thing: want %q, have %q", want, have)
}
}

View File

@@ -0,0 +1,145 @@
package lv
import "sync"
// NewSpace returns an N-dimensional vector space.
func NewSpace() *Space {
return &Space{}
}
// Space represents an N-dimensional vector space. Each name and unique label
// value pair establishes a new dimension and point within that dimension. Order
// matters, i.e. [a=1 b=2] identifies a different timeseries than [b=2 a=1].
type Space struct {
mtx sync.RWMutex
nodes map[string]*node
}
// Observe locates the time series identified by the name and label values in
// the vector space, and appends the value to the list of observations.
func (s *Space) Observe(name string, lvs LabelValues, value float64) {
s.nodeFor(name).observe(lvs, value)
}
// Add locates the time series identified by the name and label values in
// the vector space, and appends the delta to the last value in the list of
// observations.
func (s *Space) Add(name string, lvs LabelValues, delta float64) {
s.nodeFor(name).add(lvs, delta)
}
// Walk traverses the vector space and invokes fn for each non-empty time series
// which is encountered. Return false to abort the traversal.
func (s *Space) Walk(fn func(name string, lvs LabelValues, observations []float64) bool) {
s.mtx.RLock()
defer s.mtx.RUnlock()
for name, node := range s.nodes {
f := func(lvs LabelValues, observations []float64) bool { return fn(name, lvs, observations) }
if !node.walk(LabelValues{}, f) {
return
}
}
}
// Reset empties the current space and returns a new Space with the old
// contents. Reset a Space to get an immutable copy suitable for walking.
func (s *Space) Reset() *Space {
s.mtx.Lock()
defer s.mtx.Unlock()
n := NewSpace()
n.nodes, s.nodes = s.nodes, n.nodes
return n
}
func (s *Space) nodeFor(name string) *node {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.nodes == nil {
s.nodes = map[string]*node{}
}
n, ok := s.nodes[name]
if !ok {
n = &node{}
s.nodes[name] = n
}
return n
}
// node exists at a specific point in the N-dimensional vector space of all
// possible label values. The node collects observations and has child nodes
// with greater specificity.
type node struct {
mtx sync.RWMutex
observations []float64
children map[pair]*node
}
type pair struct{ label, value string }
func (n *node) observe(lvs LabelValues, value float64) {
n.mtx.Lock()
defer n.mtx.Unlock()
if len(lvs) == 0 {
n.observations = append(n.observations, value)
return
}
if len(lvs) < 2 {
panic("too few LabelValues; programmer error!")
}
head, tail := pair{lvs[0], lvs[1]}, lvs[2:]
if n.children == nil {
n.children = map[pair]*node{}
}
child, ok := n.children[head]
if !ok {
child = &node{}
n.children[head] = child
}
child.observe(tail, value)
}
func (n *node) add(lvs LabelValues, delta float64) {
n.mtx.Lock()
defer n.mtx.Unlock()
if len(lvs) == 0 {
var value float64
if len(n.observations) > 0 {
value = last(n.observations) + delta
} else {
value = delta
}
n.observations = append(n.observations, value)
return
}
if len(lvs) < 2 {
panic("too few LabelValues; programmer error!")
}
head, tail := pair{lvs[0], lvs[1]}, lvs[2:]
if n.children == nil {
n.children = map[pair]*node{}
}
child, ok := n.children[head]
if !ok {
child = &node{}
n.children[head] = child
}
child.add(tail, delta)
}
func (n *node) walk(lvs LabelValues, fn func(LabelValues, []float64) bool) bool {
n.mtx.RLock()
defer n.mtx.RUnlock()
if len(n.observations) > 0 && !fn(lvs, n.observations) {
return false
}
for p, child := range n.children {
if !child.walk(append(lvs, p.label, p.value), fn) {
return false
}
}
return true
}
func last(a []float64) float64 {
return a[len(a)-1]
}

View File

@@ -0,0 +1,86 @@
package lv
import (
"strings"
"testing"
)
func TestSpaceWalkAbort(t *testing.T) {
s := NewSpace()
s.Observe("a", LabelValues{"a", "b"}, 1)
s.Observe("a", LabelValues{"c", "d"}, 2)
s.Observe("a", LabelValues{"e", "f"}, 4)
s.Observe("a", LabelValues{"g", "h"}, 8)
s.Observe("b", LabelValues{"a", "b"}, 16)
s.Observe("b", LabelValues{"c", "d"}, 32)
s.Observe("b", LabelValues{"e", "f"}, 64)
s.Observe("b", LabelValues{"g", "h"}, 128)
var count int
s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
count++
return false
})
if want, have := 1, count; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func TestSpaceWalkSums(t *testing.T) {
s := NewSpace()
s.Observe("metric_one", LabelValues{}, 1)
s.Observe("metric_one", LabelValues{}, 2)
s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 4)
s.Observe("metric_one", LabelValues{"a", "1", "b", "2"}, 8)
s.Observe("metric_one", LabelValues{}, 16)
s.Observe("metric_one", LabelValues{"a", "1", "b", "3"}, 32)
s.Observe("metric_two", LabelValues{}, 64)
s.Observe("metric_two", LabelValues{}, 128)
s.Observe("metric_two", LabelValues{"a", "1", "b", "2"}, 256)
have := map[string]float64{}
s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
//t.Logf("%s %v => %v", name, lvs, obs)
have[name+" ["+strings.Join(lvs, "")+"]"] += sum(obs)
return true
})
want := map[string]float64{
"metric_one []": 1 + 2 + 16,
"metric_one [a1b2]": 4 + 8,
"metric_one [a1b3]": 32,
"metric_two []": 64 + 128,
"metric_two [a1b2]": 256,
}
for keystr, wantsum := range want {
if havesum := have[keystr]; wantsum != havesum {
t.Errorf("%q: want %.1f, have %.1f", keystr, wantsum, havesum)
}
delete(want, keystr)
delete(have, keystr)
}
for keystr, havesum := range have {
t.Errorf("%q: unexpected observations recorded: %.1f", keystr, havesum)
}
}
func TestSpaceWalkSkipsEmptyDimensions(t *testing.T) {
s := NewSpace()
s.Observe("foo", LabelValues{"bar", "1", "baz", "2"}, 123)
var count int
s.Walk(func(name string, lvs LabelValues, obs []float64) bool {
count++
return true
})
if want, have := 1, count; want != have {
t.Errorf("want %d, have %d", want, have)
}
}
func sum(a []float64) (v float64) {
for _, f := range a {
v += f
}
return
}

View File

@@ -0,0 +1,40 @@
// Package ratemap implements a goroutine-safe map of string to float64. It can
// be embedded in implementations whose metrics support fixed sample rates, so
// that an additional parameter doesn't have to be tracked through the e.g.
// lv.Space object.
package ratemap
import "sync"
// RateMap is a simple goroutine-safe map of string to float64.
type RateMap struct {
mtx sync.RWMutex
m map[string]float64
}
// New returns a new RateMap.
func New() *RateMap {
return &RateMap{
m: map[string]float64{},
}
}
// Set writes the given name/rate pair to the map.
// Set is safe for concurrent access by multiple goroutines.
func (m *RateMap) Set(name string, rate float64) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.m[name] = rate
}
// Get retrieves the rate for the given name, or 1.0 if none is set.
// Get is safe for concurrent access by multiple goroutines.
func (m *RateMap) Get(name string) float64 {
m.mtx.RLock()
defer m.mtx.RUnlock()
f, ok := m.m[name]
if !ok {
f = 1.0
}
return f
}

25
vendor/github.com/go-kit/kit/metrics/metrics.go generated vendored Normal file
View File

@@ -0,0 +1,25 @@
package metrics
// Counter describes a metric that accumulates values monotonically.
// An example of a counter is the number of received HTTP requests.
type Counter interface {
With(labelValues ...string) Counter
Add(delta float64)
}
// Gauge describes a metric that takes specific values over time.
// An example of a gauge is the current depth of a job queue.
type Gauge interface {
With(labelValues ...string) Gauge
Set(value float64)
Add(delta float64)
}
// Histogram describes a metric that takes repeated observations of the same
// kind of thing, and produces a statistical summary of those observations,
// typically expressed as quantiles or buckets. An example of a histogram is
// HTTP request latencies.
type Histogram interface {
With(labelValues ...string) Histogram
Observe(value float64)
}

86
vendor/github.com/go-kit/kit/metrics/multi/multi.go generated vendored Normal file
View File

@@ -0,0 +1,86 @@
// Package multi provides adapters that send observations to multiple metrics
// simultaneously. This is useful if your service needs to emit to multiple
// instrumentation systems at the same time, for example if your organization is
// transitioning from one system to another.
package multi
import "github.com/go-kit/kit/metrics"
// Counter collects multiple individual counters and treats them as a unit.
type Counter []metrics.Counter
// NewCounter returns a multi-counter, wrapping the passed counters.
func NewCounter(c ...metrics.Counter) Counter {
return Counter(c)
}
// Add implements counter.
func (c Counter) Add(delta float64) {
for _, counter := range c {
counter.Add(delta)
}
}
// With implements counter.
func (c Counter) With(labelValues ...string) metrics.Counter {
next := make(Counter, len(c))
for i := range c {
next[i] = c[i].With(labelValues...)
}
return next
}
// Gauge collects multiple individual gauges and treats them as a unit.
type Gauge []metrics.Gauge
// NewGauge returns a multi-gauge, wrapping the passed gauges.
func NewGauge(g ...metrics.Gauge) Gauge {
return Gauge(g)
}
// Set implements Gauge.
func (g Gauge) Set(value float64) {
for _, gauge := range g {
gauge.Set(value)
}
}
// With implements gauge.
func (g Gauge) With(labelValues ...string) metrics.Gauge {
next := make(Gauge, len(g))
for i := range g {
next[i] = g[i].With(labelValues...)
}
return next
}
// Add implements metrics.Gauge.
func (g Gauge) Add(delta float64) {
for _, gauge := range g {
gauge.Add(delta)
}
}
// Histogram collects multiple individual histograms and treats them as a unit.
type Histogram []metrics.Histogram
// NewHistogram returns a multi-histogram, wrapping the passed histograms.
func NewHistogram(h ...metrics.Histogram) Histogram {
return Histogram(h)
}
// Observe implements Histogram.
func (h Histogram) Observe(value float64) {
for _, histogram := range h {
histogram.Observe(value)
}
}
// With implements histogram.
func (h Histogram) With(labelValues ...string) metrics.Histogram {
next := make(Histogram, len(h))
for i := range h {
next[i] = h[i].With(labelValues...)
}
return next
}

View File

@@ -0,0 +1,96 @@
package multi
import (
"fmt"
"testing"
"github.com/go-kit/kit/metrics"
)
func TestMultiCounter(t *testing.T) {
c1 := &mockCounter{}
c2 := &mockCounter{}
c3 := &mockCounter{}
mc := NewCounter(c1, c2, c3)
mc.Add(123)
mc.Add(456)
want := "[123 456]"
for i, m := range []fmt.Stringer{c1, c2, c3} {
if have := m.String(); want != have {
t.Errorf("c%d: want %q, have %q", i+1, want, have)
}
}
}
func TestMultiGauge(t *testing.T) {
g1 := &mockGauge{}
g2 := &mockGauge{}
g3 := &mockGauge{}
mg := NewGauge(g1, g2, g3)
mg.Set(9)
mg.Set(8)
mg.Set(7)
mg.Add(3)
want := "[9 8 7 10]"
for i, m := range []fmt.Stringer{g1, g2, g3} {
if have := m.String(); want != have {
t.Errorf("g%d: want %q, have %q", i+1, want, have)
}
}
}
func TestMultiHistogram(t *testing.T) {
h1 := &mockHistogram{}
h2 := &mockHistogram{}
h3 := &mockHistogram{}
mh := NewHistogram(h1, h2, h3)
mh.Observe(1)
mh.Observe(2)
mh.Observe(4)
mh.Observe(8)
want := "[1 2 4 8]"
for i, m := range []fmt.Stringer{h1, h2, h3} {
if have := m.String(); want != have {
t.Errorf("g%d: want %q, have %q", i+1, want, have)
}
}
}
type mockCounter struct {
obs []float64
}
func (c *mockCounter) Add(delta float64) { c.obs = append(c.obs, delta) }
func (c *mockCounter) With(...string) metrics.Counter { return c }
func (c *mockCounter) String() string { return fmt.Sprintf("%v", c.obs) }
type mockGauge struct {
obs []float64
}
func (g *mockGauge) Set(value float64) { g.obs = append(g.obs, value) }
func (g *mockGauge) With(...string) metrics.Gauge { return g }
func (g *mockGauge) String() string { return fmt.Sprintf("%v", g.obs) }
func (g *mockGauge) Add(delta float64) {
var value float64
if len(g.obs) > 0 {
value = g.obs[len(g.obs)-1] + delta
} else {
value = delta
}
g.obs = append(g.obs, value)
}
type mockHistogram struct {
obs []float64
}
func (h *mockHistogram) Observe(value float64) { h.obs = append(h.obs, value) }
func (h *mockHistogram) With(...string) metrics.Histogram { return h }
func (h *mockHistogram) String() string { return fmt.Sprintf("%v", h.obs) }

125
vendor/github.com/go-kit/kit/metrics/pcp/pcp.go generated vendored Normal file
View File

@@ -0,0 +1,125 @@
package pcp
import (
"github.com/performancecopilot/speed"
"github.com/go-kit/kit/metrics"
)
// Reporter encapsulates a speed client.
type Reporter struct {
c *speed.PCPClient
}
// NewReporter creates a new Reporter instance. The first parameter is the
// application name and is used to create the speed client. Hence it should be a
// valid speed parameter name and should not contain spaces or the path
// separator for your operating system.
func NewReporter(appname string) (*Reporter, error) {
c, err := speed.NewPCPClient(appname)
if err != nil {
return nil, err
}
return &Reporter{c}, nil
}
// Start starts the underlying speed client so it can start reporting registered
// metrics to your PCP installation.
func (r *Reporter) Start() { r.c.MustStart() }
// Stop stops the underlying speed client so it can stop reporting registered
// metrics to your PCP installation.
func (r *Reporter) Stop() { r.c.MustStop() }
// Counter implements metrics.Counter via a single dimensional speed.Counter.
type Counter struct {
c speed.Counter
}
// NewCounter creates a new Counter. This requires a name parameter and can
// optionally take a couple of description strings, that are used to create the
// underlying speed.Counter and are reported by PCP.
func (r *Reporter) NewCounter(name string, desc ...string) (*Counter, error) {
c, err := speed.NewPCPCounter(0, name, desc...)
if err != nil {
return nil, err
}
r.c.MustRegister(c)
return &Counter{c}, nil
}
// With is a no-op.
func (c *Counter) With(labelValues ...string) metrics.Counter { return c }
// Add increments Counter. speed.Counters only take int64, so delta is converted
// to int64 before observation.
func (c *Counter) Add(delta float64) { c.c.Inc(int64(delta)) }
// Gauge implements metrics.Gauge via a single dimensional speed.Gauge.
type Gauge struct {
g speed.Gauge
}
// NewGauge creates a new Gauge. This requires a name parameter and can
// optionally take a couple of description strings, that are used to create the
// underlying speed.Gauge and are reported by PCP.
func (r *Reporter) NewGauge(name string, desc ...string) (*Gauge, error) {
g, err := speed.NewPCPGauge(0, name, desc...)
if err != nil {
return nil, err
}
r.c.MustRegister(g)
return &Gauge{g}, nil
}
// With is a no-op.
func (g *Gauge) With(labelValues ...string) metrics.Gauge { return g }
// Set sets the value of the gauge.
func (g *Gauge) Set(value float64) { g.g.Set(value) }
// Add adds a value to the gauge.
func (g *Gauge) Add(delta float64) { g.g.Inc(delta) }
// Histogram wraps a speed Histogram.
type Histogram struct {
h speed.Histogram
}
// NewHistogram creates a new Histogram. The minimum observeable value is 0. The
// maximum observeable value is 3600000000 (3.6e9).
//
// The required parameters are a metric name, the minimum and maximum observable
// values, and a metric unit for the units of the observed values.
//
// Optionally, it can also take a couple of description strings.
func (r *Reporter) NewHistogram(name string, min, max int64, unit speed.MetricUnit, desc ...string) (*Histogram, error) {
h, err := speed.NewPCPHistogram(name, min, max, 5, unit, desc...)
if err != nil {
return nil, err
}
r.c.MustRegister(h)
return &Histogram{h}, nil
}
// With is a no-op.
func (h *Histogram) With(labelValues ...string) metrics.Histogram { return h }
// Observe observes a value.
//
// This converts float64 value to int64 before observation, as the Histogram in
// speed is backed using codahale/hdrhistogram, which only observes int64
// values. Additionally, the value is interpreted in the metric unit used to
// construct the histogram.
func (h *Histogram) Observe(value float64) { h.h.MustRecord(int64(value)) }
// Mean returns the mean of the values observed so far by the Histogram.
func (h *Histogram) Mean() float64 { return h.h.Mean() }
// Percentile returns a percentile value for the given percentile
// between 0 and 100 for all values observed by the histogram.
func (h *Histogram) Percentile(p float64) int64 { return h.h.Percentile(p) }

72
vendor/github.com/go-kit/kit/metrics/pcp/pcp_test.go generated vendored Normal file
View File

@@ -0,0 +1,72 @@
package pcp
import (
"testing"
"github.com/performancecopilot/speed"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
r, err := NewReporter("test_counter")
if err != nil {
t.Fatal(err)
}
counter, err := r.NewCounter("speed_counter")
if err != nil {
t.Fatal(err)
}
counter = counter.With("label values", "not supported").(*Counter)
value := func() float64 { f := counter.c.Val(); return float64(f) }
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
r, err := NewReporter("test_gauge")
if err != nil {
t.Fatal(err)
}
gauge, err := r.NewGauge("speed_gauge")
if err != nil {
t.Fatal(err)
}
gauge = gauge.With("label values", "not supported").(*Gauge)
value := func() float64 { f := gauge.g.Val(); return f }
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
r, err := NewReporter("test_histogram")
if err != nil {
t.Fatal(err)
}
histogram, err := r.NewHistogram("speed_histogram", 0, 3600000000, speed.OneUnit)
if err != nil {
t.Fatal(err)
}
histogram = histogram.With("label values", "not supported").(*Histogram)
quantiles := func() (float64, float64, float64, float64) {
p50 := float64(histogram.Percentile(50))
p90 := float64(histogram.Percentile(90))
p95 := float64(histogram.Percentile(95))
p99 := float64(histogram.Percentile(99))
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(histogram, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,165 @@
// Package prometheus provides Prometheus implementations for metrics.
// Individual metrics are mapped to their Prometheus counterparts, and
// (depending on the constructor used) may be automatically registered in the
// global Prometheus metrics registry.
package prometheus
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
)
// Counter implements Counter, via a Prometheus CounterVec.
type Counter struct {
cv *prometheus.CounterVec
lvs lv.LabelValues
}
// NewCounterFrom constructs and registers a Prometheus CounterVec,
// and returns a usable Counter object.
func NewCounterFrom(opts prometheus.CounterOpts, labelNames []string) *Counter {
cv := prometheus.NewCounterVec(opts, labelNames)
prometheus.MustRegister(cv)
return NewCounter(cv)
}
// NewCounter wraps the CounterVec and returns a usable Counter object.
func NewCounter(cv *prometheus.CounterVec) *Counter {
return &Counter{
cv: cv,
}
}
// With implements Counter.
func (c *Counter) With(labelValues ...string) metrics.Counter {
return &Counter{
cv: c.cv,
lvs: c.lvs.With(labelValues...),
}
}
// Add implements Counter.
func (c *Counter) Add(delta float64) {
c.cv.With(makeLabels(c.lvs...)).Add(delta)
}
// Gauge implements Gauge, via a Prometheus GaugeVec.
type Gauge struct {
gv *prometheus.GaugeVec
lvs lv.LabelValues
}
// NewGaugeFrom construts and registers a Prometheus GaugeVec,
// and returns a usable Gauge object.
func NewGaugeFrom(opts prometheus.GaugeOpts, labelNames []string) *Gauge {
gv := prometheus.NewGaugeVec(opts, labelNames)
prometheus.MustRegister(gv)
return NewGauge(gv)
}
// NewGauge wraps the GaugeVec and returns a usable Gauge object.
func NewGauge(gv *prometheus.GaugeVec) *Gauge {
return &Gauge{
gv: gv,
}
}
// With implements Gauge.
func (g *Gauge) With(labelValues ...string) metrics.Gauge {
return &Gauge{
gv: g.gv,
lvs: g.lvs.With(labelValues...),
}
}
// Set implements Gauge.
func (g *Gauge) Set(value float64) {
g.gv.With(makeLabels(g.lvs...)).Set(value)
}
// Add is supported by Prometheus GaugeVecs.
func (g *Gauge) Add(delta float64) {
g.gv.With(makeLabels(g.lvs...)).Add(delta)
}
// Summary implements Histogram, via a Prometheus SummaryVec. The difference
// between a Summary and a Histogram is that Summaries don't require predefined
// quantile buckets, but cannot be statistically aggregated.
type Summary struct {
sv *prometheus.SummaryVec
lvs lv.LabelValues
}
// NewSummaryFrom constructs and registers a Prometheus SummaryVec,
// and returns a usable Summary object.
func NewSummaryFrom(opts prometheus.SummaryOpts, labelNames []string) *Summary {
sv := prometheus.NewSummaryVec(opts, labelNames)
prometheus.MustRegister(sv)
return NewSummary(sv)
}
// NewSummary wraps the SummaryVec and returns a usable Summary object.
func NewSummary(sv *prometheus.SummaryVec) *Summary {
return &Summary{
sv: sv,
}
}
// With implements Histogram.
func (s *Summary) With(labelValues ...string) metrics.Histogram {
return &Summary{
sv: s.sv,
lvs: s.lvs.With(labelValues...),
}
}
// Observe implements Histogram.
func (s *Summary) Observe(value float64) {
s.sv.With(makeLabels(s.lvs...)).Observe(value)
}
// Histogram implements Histogram via a Prometheus HistogramVec. The difference
// between a Histogram and a Summary is that Histograms require predefined
// quantile buckets, and can be statistically aggregated.
type Histogram struct {
hv *prometheus.HistogramVec
lvs lv.LabelValues
}
// NewHistogramFrom constructs and registers a Prometheus HistogramVec,
// and returns a usable Histogram object.
func NewHistogramFrom(opts prometheus.HistogramOpts, labelNames []string) *Histogram {
hv := prometheus.NewHistogramVec(opts, labelNames)
prometheus.MustRegister(hv)
return NewHistogram(hv)
}
// NewHistogram wraps the HistogramVec and returns a usable Histogram object.
func NewHistogram(hv *prometheus.HistogramVec) *Histogram {
return &Histogram{
hv: hv,
}
}
// With implements Histogram.
func (h *Histogram) With(labelValues ...string) metrics.Histogram {
return &Histogram{
hv: h.hv,
lvs: h.lvs.With(labelValues...),
}
}
// Observe implements Histogram.
func (h *Histogram) Observe(value float64) {
h.hv.With(makeLabels(h.lvs...)).Observe(value)
}
func makeLabels(labelValues ...string) prometheus.Labels {
labels := prometheus.Labels{}
for i := 0; i < len(labelValues); i += 2 {
labels[labelValues[i]] = labelValues[i+1]
}
return labels
}

View File

@@ -0,0 +1,214 @@
package prometheus
import (
"io/ioutil"
"math"
"math/rand"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strconv"
"strings"
"testing"
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "ns", "ss", "foo"
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{alpha="alpha-value",beta="beta-value"} ([0-9\.]+)`)
counter := NewCounterFrom(stdprometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is the help string.",
}, []string{"alpha", "beta"}).With("beta", "beta-value", "alpha", "alpha-value") // order shouldn't matter
value := func() float64 {
matches := re.FindStringSubmatch(scrape())
f, _ := strconv.ParseFloat(matches[1], 64)
return f
}
if err := teststat.TestCounter(counter, value); err != nil {
t.Fatal(err)
}
}
func TestGauge(t *testing.T) {
s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "aaa", "bbb", "ccc"
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{foo="bar"} ([0-9\.]+)`)
gauge := NewGaugeFrom(stdprometheus.GaugeOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is a different help string.",
}, []string{"foo"}).With("foo", "bar")
value := func() float64 {
matches := re.FindStringSubmatch(scrape())
f, _ := strconv.ParseFloat(matches[1], 64)
return f
}
if err := teststat.TestGauge(gauge, value); err != nil {
t.Fatal(err)
}
}
func TestSummary(t *testing.T) {
s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "test", "prometheus", "summary"
re50 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.5"} ([0-9\.]+)`)
re90 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.9"} ([0-9\.]+)`)
re99 := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `{a="a",b="b",quantile="0.99"} ([0-9\.]+)`)
summary := NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is the help string for the summary.",
}, []string{"a", "b"}).With("b", "b").With("a", "a")
quantiles := func() (float64, float64, float64, float64) {
buf := scrape()
match50 := re50.FindStringSubmatch(buf)
p50, _ := strconv.ParseFloat(match50[1], 64)
match90 := re90.FindStringSubmatch(buf)
p90, _ := strconv.ParseFloat(match90[1], 64)
match99 := re99.FindStringSubmatch(buf)
p99, _ := strconv.ParseFloat(match99[1], 64)
p95 := p90 + ((p99 - p90) / 2) // Prometheus, y u no p95??? :< #yolo
return p50, p90, p95, p99
}
if err := teststat.TestHistogram(summary, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestHistogram(t *testing.T) {
// Prometheus reports histograms as a count of observations that fell into
// each predefined bucket, with the bucket value representing a global upper
// limit. That is, the count monotonically increases over the buckets. This
// requires a different strategy to test.
s := httptest.NewServer(stdprometheus.UninstrumentedHandler())
defer s.Close()
scrape := func() string {
resp, _ := http.Get(s.URL)
buf, _ := ioutil.ReadAll(resp.Body)
return string(buf)
}
namespace, subsystem, name := "test", "prometheus", "histogram"
re := regexp.MustCompile(namespace + `_` + subsystem + `_` + name + `_bucket{x="1",le="([0-9]+|\+Inf)"} ([0-9\.]+)`)
numStdev := 3
bucketMin := (teststat.Mean - (numStdev * teststat.Stdev))
bucketMax := (teststat.Mean + (numStdev * teststat.Stdev))
if bucketMin < 0 {
bucketMin = 0
}
bucketCount := 10
bucketDelta := (bucketMax - bucketMin) / bucketCount
buckets := []float64{}
for i := bucketMin; i <= bucketMax; i += bucketDelta {
buckets = append(buckets, float64(i))
}
histogram := NewHistogramFrom(stdprometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: name,
Help: "This is the help string for the histogram.",
Buckets: buckets,
}, []string{"x"}).With("x", "1")
// Can't TestHistogram, because Prometheus Histograms don't dynamically
// compute quantiles. Instead, they fill up buckets. So, let's populate the
// histogram kind of manually.
teststat.PopulateNormalHistogram(histogram, rand.Int())
// Then, we use ExpectedObservationsLessThan to validate.
for _, line := range strings.Split(scrape(), "\n") {
match := re.FindStringSubmatch(line)
if match == nil {
continue
}
bucket, _ := strconv.ParseInt(match[1], 10, 64)
have, _ := strconv.ParseInt(match[2], 10, 64)
want := teststat.ExpectedObservationsLessThan(bucket)
if match[1] == "+Inf" {
want = int64(teststat.Count) // special case
}
// Unfortunately, we observe experimentally that Prometheus is quite
// imprecise at the extremes. I'm setting a very high tolerance for now.
// It would be great to dig in and figure out whether that's a problem
// with my Expected calculation, or in Prometheus.
tolerance := 0.25
if delta := math.Abs(float64(want) - float64(have)); (delta / float64(want)) > tolerance {
t.Errorf("Bucket %d: want %d, have %d (%.1f%%)", bucket, want, have, (100.0 * delta / float64(want)))
}
}
}
func TestInconsistentLabelCardinality(t *testing.T) {
defer func() {
x := recover()
if x == nil {
t.Fatal("expected panic, got none")
}
err, ok := x.(error)
if !ok {
t.Fatalf("expected error, got %s", reflect.TypeOf(x))
}
if want, have := "inconsistent label cardinality", err.Error(); want != have {
t.Fatalf("want %q, have %q", want, have)
}
}()
NewCounterFrom(stdprometheus.CounterOpts{
Namespace: "test",
Subsystem: "inconsistent_label_cardinality",
Name: "foobar",
Help: "This is the help string for the metric.",
}, []string{"a", "b"}).With(
"a", "1", "b", "2", "c", "KABOOM!",
).Add(123)
}

View File

@@ -0,0 +1,24 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/discard"
)
type discardProvider struct{}
// NewDiscardProvider returns a provider that produces no-op metrics via the
// discarding backend.
func NewDiscardProvider() Provider { return discardProvider{} }
// NewCounter implements Provider.
func (discardProvider) NewCounter(string) metrics.Counter { return discard.NewCounter() }
// NewGauge implements Provider.
func (discardProvider) NewGauge(string) metrics.Gauge { return discard.NewGauge() }
// NewHistogram implements Provider.
func (discardProvider) NewHistogram(string, int) metrics.Histogram { return discard.NewHistogram() }
// Stop implements Provider.
func (discardProvider) Stop() {}

View File

@@ -0,0 +1,43 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/dogstatsd"
)
type dogstatsdProvider struct {
d *dogstatsd.Dogstatsd
stop func()
}
// NewDogstatsdProvider wraps the given Dogstatsd object and stop func and
// returns a Provider that produces Dogstatsd metrics. A typical stop function
// would be ticker.Stop from the ticker passed to the SendLoop helper method.
func NewDogstatsdProvider(d *dogstatsd.Dogstatsd, stop func()) Provider {
return &dogstatsdProvider{
d: d,
stop: stop,
}
}
// NewCounter implements Provider, returning a new Dogstatsd Counter with a
// sample rate of 1.0.
func (p *dogstatsdProvider) NewCounter(name string) metrics.Counter {
return p.d.NewCounter(name, 1.0)
}
// NewGauge implements Provider.
func (p *dogstatsdProvider) NewGauge(name string) metrics.Gauge {
return p.d.NewGauge(name)
}
// NewHistogram implements Provider, returning a new Dogstatsd Histogram (note:
// not a Timing) with a sample rate of 1.0. The buckets argument is ignored.
func (p *dogstatsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
return p.d.NewHistogram(name, 1.0)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *dogstatsdProvider) Stop() {
p.stop()
}

View File

@@ -0,0 +1,31 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/expvar"
)
type expvarProvider struct{}
// NewExpvarProvider returns a Provider that produces expvar metrics.
func NewExpvarProvider() Provider {
return expvarProvider{}
}
// NewCounter implements Provider.
func (p expvarProvider) NewCounter(name string) metrics.Counter {
return expvar.NewCounter(name)
}
// NewGauge implements Provider.
func (p expvarProvider) NewGauge(name string) metrics.Gauge {
return expvar.NewGauge(name)
}
// NewHistogram implements Provider.
func (p expvarProvider) NewHistogram(name string, buckets int) metrics.Histogram {
return expvar.NewHistogram(name, buckets)
}
// Stop implements Provider, but is a no-op.
func (p expvarProvider) Stop() {}

View File

@@ -0,0 +1,41 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/graphite"
)
type graphiteProvider struct {
g *graphite.Graphite
stop func()
}
// NewGraphiteProvider wraps the given Graphite object and stop func and returns
// a Provider that produces Graphite metrics. A typical stop function would be
// ticker.Stop from the ticker passed to the SendLoop helper method.
func NewGraphiteProvider(g *graphite.Graphite, stop func()) Provider {
return &graphiteProvider{
g: g,
stop: stop,
}
}
// NewCounter implements Provider.
func (p *graphiteProvider) NewCounter(name string) metrics.Counter {
return p.g.NewCounter(name)
}
// NewGauge implements Provider.
func (p *graphiteProvider) NewGauge(name string) metrics.Gauge {
return p.g.NewGauge(name)
}
// NewHistogram implements Provider.
func (p *graphiteProvider) NewHistogram(name string, buckets int) metrics.Histogram {
return p.g.NewHistogram(name, buckets)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *graphiteProvider) Stop() {
p.stop()
}

View File

@@ -0,0 +1,40 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/influx"
)
type influxProvider struct {
in *influx.Influx
stop func()
}
// NewInfluxProvider takes the given Influx object and stop func, and returns
// a Provider that produces Influx metrics.
func NewInfluxProvider(in *influx.Influx, stop func()) Provider {
return &influxProvider{
in: in,
stop: stop,
}
}
// NewCounter implements Provider. Per-metric tags are not supported.
func (p *influxProvider) NewCounter(name string) metrics.Counter {
return p.in.NewCounter(name)
}
// NewGauge implements Provider. Per-metric tags are not supported.
func (p *influxProvider) NewGauge(name string) metrics.Gauge {
return p.in.NewGauge(name)
}
// NewHistogram implements Provider. Per-metric tags are not supported.
func (p *influxProvider) NewHistogram(name string, buckets int) metrics.Histogram {
return p.in.NewHistogram(name)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *influxProvider) Stop() {
p.stop()
}

View File

@@ -0,0 +1,63 @@
package provider
import (
stdprometheus "github.com/prometheus/client_golang/prometheus"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/prometheus"
)
type prometheusProvider struct {
namespace string
subsystem string
}
// NewPrometheusProvider returns a Provider that produces Prometheus metrics.
// Namespace and subsystem are applied to all produced metrics.
func NewPrometheusProvider(namespace, subsystem string) Provider {
return &prometheusProvider{
namespace: namespace,
subsystem: subsystem,
}
}
// NewCounter implements Provider via prometheus.NewCounterFrom, i.e. the
// counter is registered. The metric's namespace and subsystem are taken from
// the Provider. Help is set to the name of the metric, and no const label names
// are set.
func (p *prometheusProvider) NewCounter(name string) metrics.Counter {
return prometheus.NewCounterFrom(stdprometheus.CounterOpts{
Namespace: p.namespace,
Subsystem: p.subsystem,
Name: name,
Help: name,
}, []string{})
}
// NewGauge implements Provider via prometheus.NewGaugeFrom, i.e. the gauge is
// registered. The metric's namespace and subsystem are taken from the Provider.
// Help is set to the name of the metric, and no const label names are set.
func (p *prometheusProvider) NewGauge(name string) metrics.Gauge {
return prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{
Namespace: p.namespace,
Subsystem: p.subsystem,
Name: name,
Help: name,
}, []string{})
}
// NewGauge implements Provider via prometheus.NewSummaryFrom, i.e. the summary
// is registered. The metric's namespace and subsystem are taken from the
// Provider. Help is set to the name of the metric, and no const label names are
// set. Buckets are ignored.
func (p *prometheusProvider) NewHistogram(name string, _ int) metrics.Histogram {
return prometheus.NewSummaryFrom(stdprometheus.SummaryOpts{
Namespace: p.namespace,
Subsystem: p.subsystem,
Name: name,
Help: name,
}, []string{})
}
// Stop implements Provider, but is a no-op.
func (p *prometheusProvider) Stop() {}

View File

@@ -0,0 +1,42 @@
// Package provider provides a factory-like abstraction for metrics backends.
// This package is provided specifically for the needs of the NY Times framework
// Gizmo. Most normal Go kit users shouldn't need to use it.
//
// Normally, if your microservice needs to support different metrics backends,
// you can simply do different construction based on a flag. For example,
//
// var latency metrics.Histogram
// var requests metrics.Counter
// switch *metricsBackend {
// case "prometheus":
// latency = prometheus.NewSummaryVec(...)
// requests = prometheus.NewCounterVec(...)
// case "statsd":
// s := statsd.New(...)
// t := time.NewTicker(5*time.Second)
// go s.SendLoop(t.C, "tcp", "statsd.local:8125")
// latency = s.NewHistogram(...)
// requests = s.NewCounter(...)
// default:
// log.Fatal("unsupported metrics backend %q", *metricsBackend)
// }
//
package provider
import (
"github.com/go-kit/kit/metrics"
)
// Provider abstracts over constructors and lifecycle management functions for
// each supported metrics backend. It should only be used by those who need to
// swap out implementations dynamically.
//
// This is primarily useful for intermediating frameworks, and is likely
// unnecessary for most Go kit services. See the package-level doc comment for
// more typical usage instructions.
type Provider interface {
NewCounter(name string) metrics.Counter
NewGauge(name string) metrics.Gauge
NewHistogram(name string, buckets int) metrics.Histogram
Stop()
}

View File

@@ -0,0 +1,43 @@
package provider
import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/statsd"
)
type statsdProvider struct {
s *statsd.Statsd
stop func()
}
// NewStatsdProvider wraps the given Statsd object and stop func and returns a
// Provider that produces Statsd metrics. A typical stop function would be
// ticker.Stop from the ticker passed to the SendLoop helper method.
func NewStatsdProvider(s *statsd.Statsd, stop func()) Provider {
return &statsdProvider{
s: s,
stop: stop,
}
}
// NewCounter implements Provider.
func (p *statsdProvider) NewCounter(name string) metrics.Counter {
return p.s.NewCounter(name, 1.0)
}
// NewGauge implements Provider.
func (p *statsdProvider) NewGauge(name string) metrics.Gauge {
return p.s.NewGauge(name)
}
// NewHistogram implements Provider, returning a StatsD Timing that accepts
// observations in milliseconds. The sample rate is fixed at 1.0. The bucket
// parameter is ignored.
func (p *statsdProvider) NewHistogram(name string, _ int) metrics.Histogram {
return p.s.NewTiming(name, 1.0)
}
// Stop implements Provider, invoking the stop function passed at construction.
func (p *statsdProvider) Stop() {
p.stop()
}

239
vendor/github.com/go-kit/kit/metrics/statsd/statsd.go generated vendored Normal file
View File

@@ -0,0 +1,239 @@
// Package statsd provides a StatsD backend for package metrics. StatsD has no
// concept of arbitrary key-value tagging, so label values are not supported,
// and With is a no-op on all metrics.
//
// This package batches observations and emits them on some schedule to the
// remote server. This is useful even if you connect to your StatsD server over
// UDP. Emitting one network packet per observation can quickly overwhelm even
// the fastest internal network.
package statsd
import (
"fmt"
"io"
"time"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/internal/lv"
"github.com/go-kit/kit/metrics/internal/ratemap"
"github.com/go-kit/kit/util/conn"
)
// Statsd receives metrics observations and forwards them to a StatsD server.
// Create a Statsd object, use it to create metrics, and pass those metrics as
// dependencies to the components that will use them.
//
// All metrics are buffered until WriteTo is called. Counters and gauges are
// aggregated into a single observation per timeseries per write. Timings are
// buffered but not aggregated.
//
// To regularly report metrics to an io.Writer, use the WriteLoop helper method.
// To send to a StatsD server, use the SendLoop helper method.
type Statsd struct {
prefix string
rates *ratemap.RateMap
// The observations are collected in an N-dimensional vector space, even
// though they only take advantage of a single dimension (name). This is an
// implementation detail born purely from convenience. It would be more
// accurate to collect them in a map[string][]float64, but we already have
// this nice data structure and helper methods.
counters *lv.Space
gauges *lv.Space
timings *lv.Space
logger log.Logger
}
// New returns a Statsd object that may be used to create metrics. Prefix is
// applied to all created metrics. Callers must ensure that regular calls to
// WriteTo are performed, either manually or with one of the helper methods.
func New(prefix string, logger log.Logger) *Statsd {
return &Statsd{
prefix: prefix,
rates: ratemap.New(),
counters: lv.NewSpace(),
gauges: lv.NewSpace(),
timings: lv.NewSpace(),
logger: logger,
}
}
// NewCounter returns a counter, sending observations to this Statsd object.
func (s *Statsd) NewCounter(name string, sampleRate float64) *Counter {
s.rates.Set(s.prefix+name, sampleRate)
return &Counter{
name: s.prefix + name,
obs: s.counters.Observe,
}
}
// NewGauge returns a gauge, sending observations to this Statsd object.
func (s *Statsd) NewGauge(name string) *Gauge {
return &Gauge{
name: s.prefix + name,
obs: s.gauges.Observe,
add: s.gauges.Add,
}
}
// NewTiming returns a histogram whose observations are interpreted as
// millisecond durations, and are forwarded to this Statsd object.
func (s *Statsd) NewTiming(name string, sampleRate float64) *Timing {
s.rates.Set(s.prefix+name, sampleRate)
return &Timing{
name: s.prefix + name,
obs: s.timings.Observe,
}
}
// WriteLoop is a helper method that invokes WriteTo to the passed writer every
// time the passed channel fires. This method blocks until the channel is
// closed, so clients probably want to run it in its own goroutine. For typical
// usage, create a time.Ticker and pass its C channel to this method.
func (s *Statsd) WriteLoop(c <-chan time.Time, w io.Writer) {
for range c {
if _, err := s.WriteTo(w); err != nil {
s.logger.Log("during", "WriteTo", "err", err)
}
}
}
// SendLoop is a helper method that wraps WriteLoop, passing a managed
// connection to the network and address. Like WriteLoop, this method blocks
// until the channel is closed, so clients probably want to start it in its own
// goroutine. For typical usage, create a time.Ticker and pass its C channel to
// this method.
func (s *Statsd) SendLoop(c <-chan time.Time, network, address string) {
s.WriteLoop(c, conn.NewDefaultManager(network, address, s.logger))
}
// WriteTo flushes the buffered content of the metrics to the writer, in
// StatsD format. WriteTo abides best-effort semantics, so observations are
// lost if there is a problem with the write. Clients should be sure to call
// WriteTo regularly, ideally through the WriteLoop or SendLoop helper methods.
func (s *Statsd) WriteTo(w io.Writer) (count int64, err error) {
var n int
s.counters.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s:%f|c%s\n", name, sum(values), sampling(s.rates.Get(name)))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
s.gauges.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
n, err = fmt.Fprintf(w, "%s:%f|g\n", name, last(values))
if err != nil {
return false
}
count += int64(n)
return true
})
if err != nil {
return count, err
}
s.timings.Reset().Walk(func(name string, _ lv.LabelValues, values []float64) bool {
sampleRate := s.rates.Get(name)
for _, value := range values {
n, err = fmt.Fprintf(w, "%s:%f|ms%s\n", name, value, sampling(sampleRate))
if err != nil {
return false
}
count += int64(n)
}
return true
})
if err != nil {
return count, err
}
return count, err
}
func sum(a []float64) float64 {
var v float64
for _, f := range a {
v += f
}
return v
}
func last(a []float64) float64 {
return a[len(a)-1]
}
func sampling(r float64) string {
var sv string
if r < 1.0 {
sv = fmt.Sprintf("|@%f", r)
}
return sv
}
type observeFunc func(name string, lvs lv.LabelValues, value float64)
// Counter is a StatsD counter. Observations are forwarded to a Statsd object,
// and aggregated (summed) per timeseries.
type Counter struct {
name string
obs observeFunc
}
// With is a no-op.
func (c *Counter) With(...string) metrics.Counter {
return c
}
// Add implements metrics.Counter.
func (c *Counter) Add(delta float64) {
c.obs(c.name, lv.LabelValues{}, delta)
}
// Gauge is a StatsD gauge. Observations are forwarded to a Statsd object, and
// aggregated (the last observation selected) per timeseries.
type Gauge struct {
name string
obs observeFunc
add observeFunc
}
// With is a no-op.
func (g *Gauge) With(...string) metrics.Gauge {
return g
}
// Set implements metrics.Gauge.
func (g *Gauge) Set(value float64) {
g.obs(g.name, lv.LabelValues{}, value)
}
// Add implements metrics.Gauge.
func (g *Gauge) Add(delta float64) {
g.add(g.name, lv.LabelValues{}, delta)
}
// Timing is a StatsD timing, or metrics.Histogram. Observations are
// forwarded to a Statsd object, and collected (but not aggregated) per
// timeseries.
type Timing struct {
name string
obs observeFunc
}
// With is a no-op.
func (t *Timing) With(...string) metrics.Histogram {
return t
}
// Observe implements metrics.Histogram. Value is interpreted as milliseconds.
func (t *Timing) Observe(value float64) {
t.obs(t.name, lv.LabelValues{}, value)
}

View File

@@ -0,0 +1,66 @@
package statsd
import (
"testing"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics/teststat"
)
func TestCounter(t *testing.T) {
prefix, name := "abc.", "def"
label, value := "label", "value" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|c$`
s := New(prefix, log.NewNopLogger())
counter := s.NewCounter(name, 1.0).With(label, value)
valuef := teststat.SumLines(s, regex)
if err := teststat.TestCounter(counter, valuef); err != nil {
t.Fatal(err)
}
}
func TestCounterSampled(t *testing.T) {
// This will involve multiplying the observed sum by the inverse of the
// sample rate and checking against the expected value within some
// tolerance.
t.Skip("TODO")
}
func TestGauge(t *testing.T) {
prefix, name := "ghi.", "jkl"
label, value := "xyz", "abc" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|g$`
s := New(prefix, log.NewNopLogger())
gauge := s.NewGauge(name).With(label, value)
valuef := teststat.LastLine(s, regex)
if err := teststat.TestGauge(gauge, valuef); err != nil {
t.Fatal(err)
}
}
// StatsD timings just emit all observations. So, we collect them into a generic
// histogram, and run the statistics test on that.
func TestTiming(t *testing.T) {
prefix, name := "statsd.", "timing_test"
label, value := "abc", "def" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|ms$`
s := New(prefix, log.NewNopLogger())
timing := s.NewTiming(name, 1.0).With(label, value)
quantiles := teststat.Quantiles(s, regex, 50) // no |@0.X
if err := teststat.TestHistogram(timing, quantiles, 0.01); err != nil {
t.Fatal(err)
}
}
func TestTimingSampled(t *testing.T) {
prefix, name := "statsd.", "sampled_timing_test"
label, value := "foo", "bar" // ignored
regex := `^` + prefix + name + `:([0-9\.]+)\|ms\|@0\.01[0]*$`
s := New(prefix, log.NewNopLogger())
timing := s.NewTiming(name, 0.01).With(label, value)
quantiles := teststat.Quantiles(s, regex, 50)
if err := teststat.TestHistogram(timing, quantiles, 0.02); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,65 @@
package teststat
import (
"bufio"
"bytes"
"io"
"regexp"
"strconv"
"github.com/go-kit/kit/metrics/generic"
)
// SumLines expects a regex whose first capture group can be parsed as a
// float64. It will dump the WriterTo and parse each line, expecting to find a
// match. It returns the sum of all captured floats.
func SumLines(w io.WriterTo, regex string) func() float64 {
return func() float64 {
sum, _ := stats(w, regex, nil)
return sum
}
}
// LastLine expects a regex whose first capture group can be parsed as a
// float64. It will dump the WriterTo and parse each line, expecting to find a
// match. It returns the final captured float.
func LastLine(w io.WriterTo, regex string) func() float64 {
return func() float64 {
_, final := stats(w, regex, nil)
return final
}
}
// Quantiles expects a regex whose first capture group can be parsed as a
// float64. It will dump the WriterTo and parse each line, expecting to find a
// match. It observes all captured floats into a generic.Histogram with the
// given number of buckets, and returns the 50th, 90th, 95th, and 99th quantiles
// from that histogram.
func Quantiles(w io.WriterTo, regex string, buckets int) func() (float64, float64, float64, float64) {
return func() (float64, float64, float64, float64) {
h := generic.NewHistogram("quantile-test", buckets)
stats(w, regex, h)
return h.Quantile(0.50), h.Quantile(0.90), h.Quantile(0.95), h.Quantile(0.99)
}
}
func stats(w io.WriterTo, regex string, h *generic.Histogram) (sum, final float64) {
re := regexp.MustCompile(regex)
buf := &bytes.Buffer{}
w.WriteTo(buf)
//fmt.Fprintf(os.Stderr, "%s\n", buf.String())
s := bufio.NewScanner(buf)
for s.Scan() {
match := re.FindStringSubmatch(s.Text())
f, err := strconv.ParseFloat(match[1], 64)
if err != nil {
panic(err)
}
sum += f
final = f
if h != nil {
h.Observe(f)
}
}
return sum, final
}

View File

@@ -0,0 +1,72 @@
package teststat
import (
"math"
"math/rand"
"github.com/go-kit/kit/metrics"
)
// PopulateNormalHistogram makes a series of normal random observations into the
// histogram. The number of observations is determined by Count. The randomness
// is determined by Mean, Stdev, and the seed parameter.
//
// This is a low-level function, exported only for metrics that don't perform
// dynamic quantile computation, like a Prometheus Histogram (c.f. Summary). In
// most cases, you don't need to use this function, and can use TestHistogram
// instead.
func PopulateNormalHistogram(h metrics.Histogram, seed int) {
r := rand.New(rand.NewSource(int64(seed)))
for i := 0; i < Count; i++ {
sample := r.NormFloat64()*float64(Stdev) + float64(Mean)
if sample < 0 {
sample = 0
}
h.Observe(sample)
}
}
func normalQuantiles() (p50, p90, p95, p99 float64) {
return nvq(50), nvq(90), nvq(95), nvq(99)
}
func nvq(quantile int) float64 {
// https://en.wikipedia.org/wiki/Normal_distribution#Quantile_function
return float64(Mean) + float64(Stdev)*math.Sqrt2*erfinv(2*(float64(quantile)/100)-1)
}
func erfinv(y float64) float64 {
// https://stackoverflow.com/questions/5971830/need-code-for-inverse-error-function
if y < -1.0 || y > 1.0 {
panic("invalid input")
}
var (
a = [4]float64{0.886226899, -1.645349621, 0.914624893, -0.140543331}
b = [4]float64{-2.118377725, 1.442710462, -0.329097515, 0.012229801}
c = [4]float64{-1.970840454, -1.624906493, 3.429567803, 1.641345311}
d = [2]float64{3.543889200, 1.637067800}
)
const y0 = 0.7
var x, z float64
if math.Abs(y) == 1.0 {
x = -y * math.Log(0.0)
} else if y < -y0 {
z = math.Sqrt(-math.Log((1.0 + y) / 2.0))
x = -(((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
} else {
if y < y0 {
z = y * y
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)
} else {
z = math.Sqrt(-math.Log((1.0 - y) / 2.0))
x = (((c[3]*z+c[2])*z+c[1])*z + c[0]) / ((d[1]*z+d[0])*z + 1.0)
}
x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
x = x - (math.Erf(x)-y)/(2.0/math.SqrtPi*math.Exp(-x*x))
}
return x
}

View File

@@ -0,0 +1,114 @@
// Package teststat provides helpers for testing metrics backends.
package teststat
import (
"errors"
"fmt"
"math"
"math/rand"
"strings"
"github.com/go-kit/kit/metrics"
)
// TestCounter puts some deltas through the counter, and then calls the value
// func to check that the counter has the correct final value.
func TestCounter(counter metrics.Counter, value func() float64) error {
a := rand.Perm(100)
n := rand.Intn(len(a))
var want float64
for i := 0; i < n; i++ {
f := float64(a[i])
counter.Add(f)
want += f
}
if have := value(); want != have {
return fmt.Errorf("want %f, have %f", want, have)
}
return nil
}
// TestGauge puts some values through the gauge, and then calls the value func
// to check that the gauge has the correct final value.
func TestGauge(gauge metrics.Gauge, value func() float64) error {
a := rand.Perm(100)
n := rand.Intn(len(a))
var want float64
for i := 0; i < n; i++ {
f := float64(a[i])
gauge.Set(f)
want = f
}
for i := 0; i < n; i++ {
f := float64(a[i])
gauge.Add(f)
want += f
}
if have := value(); want != have {
return fmt.Errorf("want %f, have %f", want, have)
}
return nil
}
// TestHistogram puts some observations through the histogram, and then calls
// the quantiles func to checks that the histogram has computed the correct
// quantiles within some tolerance
func TestHistogram(histogram metrics.Histogram, quantiles func() (p50, p90, p95, p99 float64), tolerance float64) error {
PopulateNormalHistogram(histogram, rand.Int())
want50, want90, want95, want99 := normalQuantiles()
have50, have90, have95, have99 := quantiles()
var errs []string
if want, have := want50, have50; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p50: want %f, have %f", want, have))
}
if want, have := want90, have90; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p90: want %f, have %f", want, have))
}
if want, have := want95, have95; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p95: want %f, have %f", want, have))
}
if want, have := want99, have99; !cmp(want, have, tolerance) {
errs = append(errs, fmt.Sprintf("p99: want %f, have %f", want, have))
}
if len(errs) > 0 {
return errors.New(strings.Join(errs, "; "))
}
return nil
}
var (
// Count is the number of observations.
Count = 12345
// Mean is the center of the normal distribution of observations.
Mean = 500
// Stdev of the normal distribution of observations.
Stdev = 25
)
// ExpectedObservationsLessThan returns the number of observations that should
// have a value less than or equal to the given value, given a normal
// distribution of observations described by Count, Mean, and Stdev.
func ExpectedObservationsLessThan(bucket int64) int64 {
// https://code.google.com/p/gostat/source/browse/stat/normal.go
cdf := ((1.0 / 2.0) * (1 + math.Erf((float64(bucket)-float64(Mean))/(float64(Stdev)*math.Sqrt2))))
return int64(cdf * float64(Count))
}
func cmp(want, have, tol float64) bool {
if (math.Abs(want-have) / want) > tol {
return false
}
return true
}

28
vendor/github.com/go-kit/kit/metrics/timer.go generated vendored Normal file
View File

@@ -0,0 +1,28 @@
package metrics
import "time"
// Timer acts as a stopwatch, sending observations to a wrapped histogram.
// It's a bit of helpful syntax sugar for h.Observe(time.Since(x)).
type Timer struct {
h Histogram
t time.Time
}
// NewTimer wraps the given histogram and records the current time.
func NewTimer(h Histogram) *Timer {
return &Timer{
h: h,
t: time.Now(),
}
}
// ObserveDuration captures the number of seconds since the timer was
// constructed, and forwards that observation to the histogram.
func (t *Timer) ObserveDuration() {
d := time.Since(t.t).Seconds()
if d < 0 {
d = 0
}
t.h.Observe(d)
}

33
vendor/github.com/go-kit/kit/metrics/timer_test.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
package metrics_test
import (
"math"
"testing"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
)
func TestTimerFast(t *testing.T) {
h := generic.NewSimpleHistogram()
metrics.NewTimer(h).ObserveDuration()
tolerance := 0.050
if want, have := 0.000, h.ApproximateMovingAverage(); math.Abs(want-have) > tolerance {
t.Errorf("want %.3f, have %.3f", want, have)
}
}
func TestTimerSlow(t *testing.T) {
h := generic.NewSimpleHistogram()
timer := metrics.NewTimer(h)
time.Sleep(250 * time.Millisecond)
timer.ObserveDuration()
tolerance := 0.050
if want, have := 0.250, h.ApproximateMovingAverage(); math.Abs(want-have) > tolerance {
t.Errorf("want %.3f, have %.3f", want, have)
}
}