Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract metrics to own package and refactor implementations #1968

Merged
merged 9 commits into from Aug 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 69 additions & 0 deletions metrics/datadog.go
@@ -0,0 +1,69 @@
package metrics

import (
"time"

"github.com/containous/traefik/log"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/metrics/dogstatsd"
)

var datadogClient = dogstatsd.New("traefik.", kitlog.LoggerFunc(func(keyvals ...interface{}) error {
log.Info(keyvals)
return nil
}))

var datadogTicker *time.Ticker

// Metric names consistent with https://github.com/DataDog/integrations-extras/pull/64
const (
ddMetricsReqsName = "requests.total"
ddMetricsLatencyName = "request.duration"
ddRetriesTotalName = "backend.retries.total"
)

// RegisterDatadog registers the metrics pusher if this didn't happen yet and creates a datadog Registry instance.
func RegisterDatadog(config *types.Datadog) Registry {
if datadogTicker == nil {
datadogTicker = initDatadogClient(config)
}

registry := &standardRegistry{
enabled: true,
reqsCounter: datadogClient.NewCounter(ddMetricsReqsName, 1.0),
reqDurationHistogram: datadogClient.NewHistogram(ddMetricsLatencyName, 1.0),
retriesCounter: datadogClient.NewCounter(ddRetriesTotalName, 1.0),
}

return registry
}

func initDatadogClient(config *types.Datadog) *time.Ticker {
address := config.Address
if len(address) == 0 {
address = "localhost:8125"
}
pushInterval, err := time.ParseDuration(config.PushInterval)
if err != nil {
log.Warnf("Unable to parse %s into pushInterval, using 10s as default value", config.PushInterval)
pushInterval = 10 * time.Second
}

report := time.NewTicker(pushInterval)

safe.Go(func() {
datadogClient.SendLoop(report.C, "udp", address)
})

return report
}

// StopDatadog stops internal datadogTicker which controls the pushing of metrics to DD Agent and resets it to `nil`.
func StopDatadog() {
if datadogTicker != nil {
datadogTicker.Stop()
}
datadogTicker = nil
}
40 changes: 40 additions & 0 deletions metrics/datadog_test.go
@@ -0,0 +1,40 @@
package metrics

import (
"net/http"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you reorganize the imports?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the notice, done.

"strconv"
"testing"
"time"

"github.com/containous/traefik/types"
"github.com/stvp/go-udp-testing"
)

func TestDatadog(t *testing.T) {
udp.SetAddr(":18125")
// This is needed to make sure that UDP Listener listens for data a bit longer, otherwise it will quit after a millisecond
udp.Timeout = 5 * time.Second

datadogRegistry := RegisterDatadog(&types.Datadog{Address: ":18125", PushInterval: "1s"})
defer StopDatadog()

if !datadogRegistry.IsEnabled() {
t.Errorf("DatadogRegistry should return true for IsEnabled()")
}

expected := []string{
// We are only validating counts, as it is nearly impossible to validate latency, since it varies every run
"traefik.requests.total:1.000000|c|#service:test,code:404,method:GET\n",
"traefik.requests.total:1.000000|c|#service:test,code:200,method:GET\n",
"traefik.backend.retries.total:2.000000|c|#service:test\n",
"traefik.request.duration:10000.000000|h|#service:test,code:200",
}

udp.ShouldReceiveAll(t, expected, func() {
datadogRegistry.ReqsCounter().With("service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
datadogRegistry.ReqsCounter().With("service", "test", "code", strconv.Itoa(http.StatusNotFound), "method", http.MethodGet).Add(1)
datadogRegistry.ReqDurationHistogram().With("service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
datadogRegistry.RetriesCounter().With("service", "test").Add(1)
datadogRegistry.RetriesCounter().With("service", "test").Add(1)
})
}
79 changes: 79 additions & 0 deletions metrics/metrics.go
@@ -0,0 +1,79 @@
package metrics

import (
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/multi"
)

// Registry has to implemented by any system that wants to monitor and expose metrics.
type Registry interface {
// IsEnabled shows whether metrics instrumentation is enabled.
IsEnabled() bool
ReqsCounter() metrics.Counter
ReqDurationHistogram() metrics.Histogram
RetriesCounter() metrics.Counter
}

// NewMultiRegistry creates a new standardRegistry that wraps multiple Registries.
func NewMultiRegistry(registries []Registry) Registry {
reqsCounters := []metrics.Counter{}
reqDurationHistograms := []metrics.Histogram{}
retriesCounters := []metrics.Counter{}

for _, r := range registries {
reqsCounters = append(reqsCounters, r.ReqsCounter())
reqDurationHistograms = append(reqDurationHistograms, r.ReqDurationHistogram())
retriesCounters = append(retriesCounters, r.RetriesCounter())
}

return &standardRegistry{
enabled: true,
reqsCounter: multi.NewCounter(reqsCounters...),
reqDurationHistogram: multi.NewHistogram(reqDurationHistograms...),
retriesCounter: multi.NewCounter(retriesCounters...),
}
}

type standardRegistry struct {
enabled bool
reqsCounter metrics.Counter
reqDurationHistogram metrics.Histogram
retriesCounter metrics.Counter
}

func (r *standardRegistry) IsEnabled() bool {
return r.enabled
}

func (r *standardRegistry) ReqsCounter() metrics.Counter {
return r.reqsCounter
}

func (r *standardRegistry) ReqDurationHistogram() metrics.Histogram {
return r.reqDurationHistogram
}

func (r *standardRegistry) RetriesCounter() metrics.Counter {
return r.retriesCounter
}

// NewVoidRegistry is a noop implementation of metrics.Registry.
// It is used to avoid nil checking in components that do metric collections.
func NewVoidRegistry() Registry {
return &standardRegistry{
enabled: false,
reqsCounter: &voidCounter{},
reqDurationHistogram: &voidHistogram{},
retriesCounter: &voidCounter{},
}
}

type voidCounter struct{}

func (v *voidCounter) With(labelValues ...string) metrics.Counter { return v }
func (v *voidCounter) Add(delta float64) {}

type voidHistogram struct{}

func (h *voidHistogram) With(labelValues ...string) metrics.Histogram { return h }
func (h *voidHistogram) Observe(value float64) {}
87 changes: 87 additions & 0 deletions metrics/metrics_test.go
@@ -0,0 +1,87 @@
package metrics

import (
"testing"

"github.com/go-kit/kit/metrics"
"github.com/stretchr/testify/assert"
)

func TestNewVoidRegistry(t *testing.T) {
registry := NewVoidRegistry()

if registry.IsEnabled() {
t.Errorf("VoidRegistry should not return true for IsEnabled()")
}
registry.ReqsCounter().With("some", "value").Add(1)
registry.ReqDurationHistogram().With("some", "value").Observe(1)
registry.RetriesCounter().With("some", "value").Add(1)
}

func TestNewMultiRegistry(t *testing.T) {
registries := []Registry{newCollectingRetryMetrics(), newCollectingRetryMetrics()}
registry := NewMultiRegistry(registries)

registry.ReqsCounter().With("key", "requests").Add(1)
registry.ReqDurationHistogram().With("key", "durations").Observe(2)
registry.RetriesCounter().With("key", "retries").Add(3)

for _, collectingRegistry := range registries {
cReqsCounter := collectingRegistry.ReqsCounter().(*counterMock)
cReqDurationHistogram := collectingRegistry.ReqDurationHistogram().(*histogramMock)
cRetriesCounter := collectingRegistry.RetriesCounter().(*counterMock)

wantCounterValue := float64(1)
if cReqsCounter.counterValue != wantCounterValue {
t.Errorf("Got value %f for ReqsCounter, want %f", cReqsCounter.counterValue, wantCounterValue)
}
wantHistogramValue := float64(2)
if cReqDurationHistogram.lastHistogramValue != wantHistogramValue {
t.Errorf("Got last observation %f for ReqDurationHistogram, want %f", cReqDurationHistogram.lastHistogramValue, wantHistogramValue)
}
wantCounterValue = float64(3)
if cRetriesCounter.counterValue != wantCounterValue {
t.Errorf("Got value %f for RetriesCounter, want %f", cRetriesCounter.counterValue, wantCounterValue)
}

assert.Equal(t, []string{"key", "requests"}, cReqsCounter.lastLabelValues)
assert.Equal(t, []string{"key", "durations"}, cReqDurationHistogram.lastLabelValues)
assert.Equal(t, []string{"key", "retries"}, cRetriesCounter.lastLabelValues)
}
}

func newCollectingRetryMetrics() Registry {
return &standardRegistry{
reqsCounter: &counterMock{},
reqDurationHistogram: &histogramMock{},
retriesCounter: &counterMock{},
}
}

type counterMock struct {
counterValue float64
lastLabelValues []string
}

func (c *counterMock) With(labelValues ...string) metrics.Counter {
c.lastLabelValues = labelValues
return c
}

func (c *counterMock) Add(delta float64) {
c.counterValue += delta
}

type histogramMock struct {
lastHistogramValue float64
lastLabelValues []string
}

func (c *histogramMock) With(labelValues ...string) metrics.Histogram {
c.lastLabelValues = labelValues
return c
}

func (c *histogramMock) Observe(value float64) {
c.lastHistogramValue = value
}
45 changes: 45 additions & 0 deletions metrics/prometheus.go
@@ -0,0 +1,45 @@
package metrics

import (
"github.com/containous/traefik/types"
"github.com/go-kit/kit/metrics/prometheus"
stdprometheus "github.com/prometheus/client_golang/prometheus"
)

const (
metricNamePrefix = "traefik_"

reqsTotalName = metricNamePrefix + "requests_total"
reqDurationName = metricNamePrefix + "request_duration_seconds"
retriesTotalName = metricNamePrefix + "backend_retries_total"
)

// RegisterPrometheus registers all Prometheus metrics.
// It must be called only once and failing to register the metrics will lead to a panic.
func RegisterPrometheus(config *types.Prometheus) Registry {
buckets := []float64{0.1, 0.3, 1.2, 5.0}
if config.Buckets != nil {
buckets = config.Buckets
}

reqCounter := prometheus.NewCounterFrom(stdprometheus.CounterOpts{
Name: reqsTotalName,
Help: "How many HTTP requests processed, partitioned by status code and method.",
}, []string{"service", "code", "method"})
reqDurationHistogram := prometheus.NewHistogramFrom(stdprometheus.HistogramOpts{
Name: reqDurationName,
Help: "How long it took to process the request.",
Buckets: buckets,
}, []string{"service", "code"})
retryCounter := prometheus.NewCounterFrom(stdprometheus.CounterOpts{
Name: retriesTotalName,
Help: "How many request retries happened in total.",
}, []string{"service"})

return &standardRegistry{
enabled: true,
reqsCounter: reqCounter,
reqDurationHistogram: reqDurationHistogram,
retriesCounter: retryCounter,
}
}