Extract metrics to own package and refactor implementations
This commit is contained in:
parent
c1b5b740ff
commit
e6c2040ea8
19 changed files with 599 additions and 797 deletions
69
metrics/datadog.go
Normal file
69
metrics/datadog.go
Normal file
|
@ -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
metrics/datadog_test.go
Normal file
40
metrics/datadog_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"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
metrics/metrics.go
Normal file
79
metrics/metrics.go
Normal file
|
@ -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
metrics/metrics_test.go
Normal file
87
metrics/metrics_test.go
Normal file
|
@ -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
metrics/prometheus.go
Normal file
45
metrics/prometheus.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
104
metrics/prometheus_test.go
Normal file
104
metrics/prometheus_test.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/types"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrometheus(t *testing.T) {
|
||||||
|
prometheusRegistry := RegisterPrometheus(&types.Prometheus{})
|
||||||
|
|
||||||
|
if !prometheusRegistry.IsEnabled() {
|
||||||
|
t.Errorf("PrometheusRegistry should return true for IsEnabled()")
|
||||||
|
}
|
||||||
|
prometheusRegistry.ReqsCounter().With("service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
|
||||||
|
prometheusRegistry.ReqsCounter().With("service", "test", "code", strconv.Itoa(http.StatusOK), "method", http.MethodGet).Add(1)
|
||||||
|
prometheusRegistry.ReqDurationHistogram().With("service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
|
||||||
|
prometheusRegistry.ReqDurationHistogram().With("service", "test", "code", strconv.Itoa(http.StatusOK)).Observe(10000)
|
||||||
|
prometheusRegistry.RetriesCounter().With("service", "test").Add(1)
|
||||||
|
|
||||||
|
metricsFamilies, err := prometheus.DefaultGatherer.Gather()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not gather metrics families: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
labels map[string]string
|
||||||
|
assert func(*dto.MetricFamily)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: reqsTotalName,
|
||||||
|
labels: map[string]string{
|
||||||
|
"code": "200",
|
||||||
|
"method": http.MethodGet,
|
||||||
|
"service": "test",
|
||||||
|
},
|
||||||
|
assert: func(family *dto.MetricFamily) {
|
||||||
|
cv := family.Metric[0].Counter.GetValue()
|
||||||
|
expectedCv := float64(2)
|
||||||
|
if cv != expectedCv {
|
||||||
|
t.Errorf("gathered metrics do not contain correct value for total requests, got %f expected %f", cv, expectedCv)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: reqDurationName,
|
||||||
|
labels: map[string]string{
|
||||||
|
"service": "test",
|
||||||
|
"code": "200",
|
||||||
|
},
|
||||||
|
assert: func(family *dto.MetricFamily) {
|
||||||
|
sc := family.Metric[0].Histogram.GetSampleCount()
|
||||||
|
expectedSc := uint64(2)
|
||||||
|
if sc != expectedSc {
|
||||||
|
t.Errorf("gathered metrics do not contain correct sample count for request duration, got %d expected %d", sc, expectedSc)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: retriesTotalName,
|
||||||
|
labels: map[string]string{
|
||||||
|
"service": "test",
|
||||||
|
},
|
||||||
|
assert: func(family *dto.MetricFamily) {
|
||||||
|
cv := family.Metric[0].Counter.GetValue()
|
||||||
|
expectedCv := float64(1)
|
||||||
|
if cv != expectedCv {
|
||||||
|
t.Errorf("gathered metrics do not contain correct value for total retries, got %f expected %f", cv, expectedCv)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
family := findMetricFamily(test.name, metricsFamilies)
|
||||||
|
if family == nil {
|
||||||
|
t.Errorf("gathered metrics do not contain %q", test.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, label := range family.Metric[0].Label {
|
||||||
|
val, ok := test.labels[*label.Name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%q metric contains unexpected label %q", test.name, *label.Name)
|
||||||
|
} else if val != *label.Value {
|
||||||
|
t.Errorf("label %q in metric %q has wrong value %q, expected %q", *label.Name, test.name, *label.Value, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test.assert(family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findMetricFamily(name string, families []*dto.MetricFamily) *dto.MetricFamily {
|
||||||
|
for _, family := range families {
|
||||||
|
if family.GetName() == name {
|
||||||
|
return family
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
61
metrics/statsd.go
Normal file
61
metrics/statsd.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
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/statsd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var statsdClient = statsd.New("traefik.", kitlog.LoggerFunc(func(keyvals ...interface{}) error {
|
||||||
|
log.Info(keyvals)
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
var statsdTicker *time.Ticker
|
||||||
|
|
||||||
|
// RegisterStatsd registers the metrics pusher if this didn't happen yet and creates a statsd Registry instance.
|
||||||
|
func RegisterStatsd(config *types.Statsd) Registry {
|
||||||
|
if statsdTicker == nil {
|
||||||
|
statsdTicker = initStatsdTicker(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &standardRegistry{
|
||||||
|
enabled: true,
|
||||||
|
reqsCounter: statsdClient.NewCounter(ddMetricsReqsName, 1.0),
|
||||||
|
reqDurationHistogram: statsdClient.NewTiming(ddMetricsLatencyName, 1.0),
|
||||||
|
retriesCounter: statsdClient.NewCounter(ddRetriesTotalName, 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initStatsdTicker initializes metrics pusher and creates a statsdClient if not created already
|
||||||
|
func initStatsdTicker(config *types.Statsd) *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() {
|
||||||
|
statsdClient.SendLoop(report.C, "udp", address)
|
||||||
|
})
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopStatsd stops internal statsdTicker which controls the pushing of metrics to StatsD Agent and resets it to `nil`
|
||||||
|
func StopStatsd() {
|
||||||
|
if statsdTicker != nil {
|
||||||
|
statsdTicker.Stop()
|
||||||
|
}
|
||||||
|
statsdTicker = nil
|
||||||
|
}
|
38
metrics/statsd_test.go
Normal file
38
metrics/statsd_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/containous/traefik/types"
|
||||||
|
"github.com/stvp/go-udp-testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatsD(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
|
||||||
|
|
||||||
|
statsdRegistry := RegisterStatsd(&types.Statsd{Address: ":18125", PushInterval: "1s"})
|
||||||
|
defer StopStatsd()
|
||||||
|
|
||||||
|
if !statsdRegistry.IsEnabled() {
|
||||||
|
t.Errorf("PrometheusRegistry 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:2.000000|c\n",
|
||||||
|
"traefik.backend.retries.total:2.000000|c\n",
|
||||||
|
"traefik.request.duration:10000.000000|ms",
|
||||||
|
}
|
||||||
|
|
||||||
|
udp.ShouldReceiveAll(t, expected, func() {
|
||||||
|
statsdRegistry.ReqsCounter().With("service", "test", "code", string(http.StatusOK), "method", http.MethodGet).Add(1)
|
||||||
|
statsdRegistry.ReqsCounter().With("service", "test", "code", string(http.StatusNotFound), "method", http.MethodGet).Add(1)
|
||||||
|
statsdRegistry.RetriesCounter().With("service", "test").Add(1)
|
||||||
|
statsdRegistry.RetriesCounter().With("service", "test").Add(1)
|
||||||
|
statsdRegistry.ReqDurationHistogram().With("service", "test", "code", string(http.StatusOK)).Observe(10000)
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,95 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
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"
|
|
||||||
"github.com/go-kit/kit/metrics/dogstatsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Metrics = (*Datadog)(nil)
|
|
||||||
var _ RetryMetrics = (*Datadog)(nil)
|
|
||||||
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Datadog is an Implementation for Metrics that exposes datadog metrics for the latency
|
|
||||||
// and the number of requests partitioned by status code and method.
|
|
||||||
// - number of requests partitioned by status code and method
|
|
||||||
// - request durations
|
|
||||||
// - amount of retries happened
|
|
||||||
type Datadog struct {
|
|
||||||
reqsCounter metrics.Counter
|
|
||||||
reqDurationHistogram metrics.Histogram
|
|
||||||
retryCounter metrics.Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dd *Datadog) getReqsCounter() metrics.Counter {
|
|
||||||
return dd.reqsCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dd *Datadog) getReqDurationHistogram() metrics.Histogram {
|
|
||||||
return dd.reqDurationHistogram
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dd *Datadog) getRetryCounter() metrics.Counter {
|
|
||||||
return dd.retryCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDataDog creates new instance of Datadog
|
|
||||||
func NewDataDog(name string) *Datadog {
|
|
||||||
var m Datadog
|
|
||||||
|
|
||||||
m.reqsCounter = datadogClient.NewCounter(ddMetricsReqsName, 1.0).With("service", name)
|
|
||||||
m.reqDurationHistogram = datadogClient.NewHistogram(ddMetricsLatencyName, 1.0).With("service", name)
|
|
||||||
m.retryCounter = datadogClient.NewCounter(ddRetriesTotalName, 1.0).With("service", name)
|
|
||||||
|
|
||||||
return &m
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitDatadogClient initializes metrics pusher and creates a datadogClient if not created already
|
|
||||||
func InitDatadogClient(config *types.Datadog) *time.Ticker {
|
|
||||||
if datadogTicker == nil {
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
datadogTicker = report
|
|
||||||
}
|
|
||||||
return datadogTicker
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopDatadogClient stops internal datadogTicker which controls the pushing of metrics to DD Agent and resets it to `nil`
|
|
||||||
func StopDatadogClient() {
|
|
||||||
if datadogTicker != nil {
|
|
||||||
datadogTicker.Stop()
|
|
||||||
}
|
|
||||||
datadogTicker = nil
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/testhelpers"
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
"github.com/stvp/go-udp-testing"
|
|
||||||
"github.com/urfave/negroni"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
InitDatadogClient(&types.Datadog{":18125", "1s"})
|
|
||||||
|
|
||||||
n := negroni.New()
|
|
||||||
dd := NewDataDog("test")
|
|
||||||
defer StopDatadogClient()
|
|
||||||
metricsMiddlewareBackend := NewMetricsWrapper(dd)
|
|
||||||
|
|
||||||
n.Use(metricsMiddlewareBackend)
|
|
||||||
r := http.NewServeMux()
|
|
||||||
r.HandleFunc(`/ok`, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprintln(w, "ok")
|
|
||||||
})
|
|
||||||
r.HandleFunc(`/not-found`, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
fmt.Fprintln(w, "not-found")
|
|
||||||
})
|
|
||||||
n.UseHandler(r)
|
|
||||||
|
|
||||||
req1 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/ok", nil)
|
|
||||||
req2 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/not-found", nil)
|
|
||||||
|
|
||||||
retryListener := NewMetricsRetryListener(dd)
|
|
||||||
retryListener.Retried(1)
|
|
||||||
retryListener.Retried(2)
|
|
||||||
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
|
|
||||||
udp.ShouldReceiveAll(t, expected, func() {
|
|
||||||
n.ServeHTTP(recorder, req1)
|
|
||||||
n.ServeHTTP(recorder, req2)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -5,77 +5,23 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-kit/kit/metrics"
|
"github.com/containous/traefik/metrics"
|
||||||
"github.com/go-kit/kit/metrics/multi"
|
gokitmetrics "github.com/go-kit/kit/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Metrics is an Interface that must be satisfied by any system that
|
|
||||||
// wants to expose and monitor Metrics.
|
|
||||||
type Metrics interface {
|
|
||||||
getReqsCounter() metrics.Counter
|
|
||||||
getReqDurationHistogram() metrics.Histogram
|
|
||||||
RetryMetrics
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetryMetrics must be satisfied by any system that wants to collect and
|
|
||||||
// expose retry specific Metrics.
|
|
||||||
type RetryMetrics interface {
|
|
||||||
getRetryCounter() metrics.Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
// MultiMetrics is a struct that provides a wrapper container for multiple Metrics, if they are configured
|
|
||||||
type MultiMetrics struct {
|
|
||||||
wrappedMetrics *[]Metrics
|
|
||||||
reqsCounter metrics.Counter
|
|
||||||
reqDurationHistogram metrics.Histogram
|
|
||||||
retryCounter metrics.Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMultiMetrics creates a new instance of MultiMetrics
|
|
||||||
func NewMultiMetrics(manyMetrics []Metrics) *MultiMetrics {
|
|
||||||
counters := []metrics.Counter{}
|
|
||||||
histograms := []metrics.Histogram{}
|
|
||||||
retryCounters := []metrics.Counter{}
|
|
||||||
|
|
||||||
for _, m := range manyMetrics {
|
|
||||||
counters = append(counters, m.getReqsCounter())
|
|
||||||
histograms = append(histograms, m.getReqDurationHistogram())
|
|
||||||
retryCounters = append(retryCounters, m.getRetryCounter())
|
|
||||||
}
|
|
||||||
|
|
||||||
var mm MultiMetrics
|
|
||||||
|
|
||||||
mm.wrappedMetrics = &manyMetrics
|
|
||||||
mm.reqsCounter = multi.NewCounter(counters...)
|
|
||||||
mm.reqDurationHistogram = multi.NewHistogram(histograms...)
|
|
||||||
mm.retryCounter = multi.NewCounter(retryCounters...)
|
|
||||||
|
|
||||||
return &mm
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *MultiMetrics) getReqsCounter() metrics.Counter {
|
|
||||||
return mm.reqsCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *MultiMetrics) getReqDurationHistogram() metrics.Histogram {
|
|
||||||
return mm.reqDurationHistogram
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mm *MultiMetrics) getRetryCounter() metrics.Counter {
|
|
||||||
return mm.retryCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetricsWrapper is a Negroni compatible Handler which relies on a
|
// MetricsWrapper is a Negroni compatible Handler which relies on a
|
||||||
// given Metrics implementation to expose and monitor Traefik Metrics.
|
// given Metrics implementation to expose and monitor Traefik Metrics.
|
||||||
type MetricsWrapper struct {
|
type MetricsWrapper struct {
|
||||||
Impl Metrics
|
registry metrics.Registry
|
||||||
|
serviceName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMetricsWrapper return a MetricsWrapper struct with
|
// NewMetricsWrapper return a MetricsWrapper struct with
|
||||||
// a given Metrics implementation e.g Prometheuss
|
// a given Metrics implementation e.g Prometheuss
|
||||||
func NewMetricsWrapper(impl Metrics) *MetricsWrapper {
|
func NewMetricsWrapper(registry metrics.Registry, service string) *MetricsWrapper {
|
||||||
var metricsWrapper = MetricsWrapper{
|
var metricsWrapper = MetricsWrapper{
|
||||||
Impl: impl,
|
registry: registry,
|
||||||
|
serviceName: service,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &metricsWrapper
|
return &metricsWrapper
|
||||||
|
@ -86,27 +32,30 @@ func (m *MetricsWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request, next
|
||||||
prw := &responseRecorder{rw, http.StatusOK}
|
prw := &responseRecorder{rw, http.StatusOK}
|
||||||
next(prw, r)
|
next(prw, r)
|
||||||
|
|
||||||
reqLabels := []string{"code", strconv.Itoa(prw.statusCode), "method", r.Method}
|
reqLabels := []string{"service", m.serviceName, "code", strconv.Itoa(prw.statusCode), "method", r.Method}
|
||||||
m.Impl.getReqsCounter().With(reqLabels...).Add(1)
|
m.registry.ReqsCounter().With(reqLabels...).Add(1)
|
||||||
|
|
||||||
reqDurationLabels := []string{"code", strconv.Itoa(prw.statusCode)}
|
reqDurationLabels := []string{"service", m.serviceName, "code", strconv.Itoa(prw.statusCode)}
|
||||||
m.Impl.getReqDurationHistogram().With(reqDurationLabels...).Observe(float64(time.Since(start).Seconds()))
|
m.registry.ReqDurationHistogram().With(reqDurationLabels...).Observe(float64(time.Since(start).Seconds()))
|
||||||
|
}
|
||||||
|
|
||||||
|
type retryMetrics interface {
|
||||||
|
RetriesCounter() gokitmetrics.Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMetricsRetryListener instantiates a MetricsRetryListener with the given retryMetrics.
|
||||||
|
func NewMetricsRetryListener(retryMetrics retryMetrics, backendName string) RetryListener {
|
||||||
|
return &MetricsRetryListener{retryMetrics: retryMetrics, backendName: backendName}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MetricsRetryListener is an implementation of the RetryListener interface to
|
// MetricsRetryListener is an implementation of the RetryListener interface to
|
||||||
// record Metrics about retry attempts.
|
// record RequestMetrics about retry attempts.
|
||||||
type MetricsRetryListener struct {
|
type MetricsRetryListener struct {
|
||||||
retryMetrics RetryMetrics
|
retryMetrics retryMetrics
|
||||||
|
backendName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retried tracks the retry in the Metrics implementation.
|
// Retried tracks the retry in the RequestMetrics implementation.
|
||||||
func (m *MetricsRetryListener) Retried(attempt int) {
|
func (m *MetricsRetryListener) Retried(attempt int) {
|
||||||
if m.retryMetrics != nil {
|
m.retryMetrics.RetriesCounter().With("backend", m.backendName).Add(1)
|
||||||
m.retryMetrics.getRetryCounter().Add(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMetricsRetryListener instantiates a MetricsRetryListener with the given RetryMetrics.
|
|
||||||
func NewMetricsRetryListener(retryMetrics RetryMetrics) RetryListener {
|
|
||||||
return &MetricsRetryListener{retryMetrics: retryMetrics}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
package middlewares
|
package middlewares
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-kit/kit/metrics"
|
"github.com/go-kit/kit/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMetricsRetryListener(t *testing.T) {
|
func TestMetricsRetryListener(t *testing.T) {
|
||||||
// nil implementation, nothing should fail
|
retryMetrics := newCollectingRetryMetrics()
|
||||||
retryListener := NewMetricsRetryListener(nil)
|
retryListener := NewMetricsRetryListener(retryMetrics, "backendName")
|
||||||
retryListener.Retried(1)
|
|
||||||
|
|
||||||
retryMetrics := newCollectingMetrics()
|
|
||||||
retryListener = NewMetricsRetryListener(retryMetrics)
|
|
||||||
retryListener.Retried(1)
|
retryListener.Retried(1)
|
||||||
retryListener.Retried(2)
|
retryListener.Retried(2)
|
||||||
|
|
||||||
|
@ -20,27 +17,34 @@ func TestMetricsRetryListener(t *testing.T) {
|
||||||
if retryMetrics.retryCounter.counterValue != wantCounterValue {
|
if retryMetrics.retryCounter.counterValue != wantCounterValue {
|
||||||
t.Errorf("got counter value of %d, want %d", retryMetrics.retryCounter.counterValue, wantCounterValue)
|
t.Errorf("got counter value of %d, want %d", retryMetrics.retryCounter.counterValue, wantCounterValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wantLabelValues := []string{"backend", "backendName"}
|
||||||
|
if !reflect.DeepEqual(retryMetrics.retryCounter.lastLabelValues, wantLabelValues) {
|
||||||
|
t.Errorf("wrong label values %v used, want %v", retryMetrics.retryCounter.lastLabelValues, wantLabelValues)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectingRetryMetrics is an implementation of the RetryMetrics interface that can be used inside tests to collect the times Add() was called.
|
// collectingRetryMetrics is an implementation of the retryMetrics interface that can be used inside tests to collect the times Add() was called.
|
||||||
type collectingRetryMetrics struct {
|
type collectingRetryMetrics struct {
|
||||||
retryCounter *collectingCounter
|
retryCounter *collectingCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCollectingMetrics() collectingRetryMetrics {
|
func newCollectingRetryMetrics() collectingRetryMetrics {
|
||||||
return collectingRetryMetrics{retryCounter: &collectingCounter{}}
|
return collectingRetryMetrics{retryCounter: &collectingCounter{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (metrics collectingRetryMetrics) getRetryCounter() metrics.Counter {
|
func (metrics collectingRetryMetrics) RetriesCounter() metrics.Counter {
|
||||||
return metrics.retryCounter
|
return metrics.retryCounter
|
||||||
}
|
}
|
||||||
|
|
||||||
type collectingCounter struct {
|
type collectingCounter struct {
|
||||||
counterValue float64
|
counterValue float64
|
||||||
|
lastLabelValues []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *collectingCounter) With(labelValues ...string) metrics.Counter {
|
func (c *collectingCounter) With(labelValues ...string) metrics.Counter {
|
||||||
panic("collectingCounter.With not implemented!")
|
c.lastLabelValues = labelValues
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *collectingCounter) Add(delta float64) {
|
func (c *collectingCounter) Add(delta float64) {
|
||||||
|
|
|
@ -1,129 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
"github.com/go-kit/kit/metrics"
|
|
||||||
"github.com/go-kit/kit/metrics/prometheus"
|
|
||||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
reqsTotalName = "traefik_requests_total"
|
|
||||||
reqDurationName = "traefik_request_duration_seconds"
|
|
||||||
retriesTotalName = "traefik_backend_retries_total"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prometheus is an Implementation for Metrics that exposes the following Prometheus metrics:
|
|
||||||
// - number of requests partitioned by status code and method
|
|
||||||
// - request durations partitioned by status code
|
|
||||||
// - amount of retries happened
|
|
||||||
type Prometheus struct {
|
|
||||||
reqsCounter metrics.Counter
|
|
||||||
reqDurationHistogram metrics.Histogram
|
|
||||||
retryCounter metrics.Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Prometheus) getReqsCounter() metrics.Counter {
|
|
||||||
return p.reqsCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Prometheus) getReqDurationHistogram() metrics.Histogram {
|
|
||||||
return p.reqDurationHistogram
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Prometheus) getRetryCounter() metrics.Counter {
|
|
||||||
return p.retryCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPrometheus returns a new Prometheus Metrics implementation.
|
|
||||||
// With the returned collectors you have the possibility to clean up the internal Prometheus state by unsubscribing the collectors.
|
|
||||||
// This is for example useful while testing the Prometheus implementation.
|
|
||||||
// If any of the Prometheus Metrics can not be registered an error will be returned and the returned Metrics implementation will be nil.
|
|
||||||
func NewPrometheus(name string, config *types.Prometheus) (*Prometheus, []stdprometheus.Collector, error) {
|
|
||||||
var prom Prometheus
|
|
||||||
var collectors []stdprometheus.Collector
|
|
||||||
|
|
||||||
cv := stdprometheus.NewCounterVec(
|
|
||||||
stdprometheus.CounterOpts{
|
|
||||||
Name: reqsTotalName,
|
|
||||||
Help: "How many HTTP requests processed, partitioned by status code and method.",
|
|
||||||
ConstLabels: stdprometheus.Labels{"service": name},
|
|
||||||
},
|
|
||||||
[]string{"code", "method"},
|
|
||||||
)
|
|
||||||
cv, err := registerCounterVec(cv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, collectors, err
|
|
||||||
}
|
|
||||||
prom.reqsCounter = prometheus.NewCounter(cv)
|
|
||||||
collectors = append(collectors, cv)
|
|
||||||
|
|
||||||
var buckets []float64
|
|
||||||
if config.Buckets != nil {
|
|
||||||
buckets = config.Buckets
|
|
||||||
} else {
|
|
||||||
buckets = []float64{0.1, 0.3, 1.2, 5}
|
|
||||||
}
|
|
||||||
hv := stdprometheus.NewHistogramVec(
|
|
||||||
stdprometheus.HistogramOpts{
|
|
||||||
Name: reqDurationName,
|
|
||||||
Help: "How long it took to process the request.",
|
|
||||||
ConstLabels: stdprometheus.Labels{"service": name},
|
|
||||||
Buckets: buckets,
|
|
||||||
},
|
|
||||||
[]string{"code"},
|
|
||||||
)
|
|
||||||
hv, err = registerHistogramVec(hv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, collectors, err
|
|
||||||
}
|
|
||||||
prom.reqDurationHistogram = prometheus.NewHistogram(hv)
|
|
||||||
collectors = append(collectors, hv)
|
|
||||||
|
|
||||||
cv = stdprometheus.NewCounterVec(
|
|
||||||
stdprometheus.CounterOpts{
|
|
||||||
Name: retriesTotalName,
|
|
||||||
Help: "How many request retries happened in total.",
|
|
||||||
ConstLabels: stdprometheus.Labels{"service": name},
|
|
||||||
},
|
|
||||||
[]string{},
|
|
||||||
)
|
|
||||||
cv, err = registerCounterVec(cv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, collectors, err
|
|
||||||
}
|
|
||||||
prom.retryCounter = prometheus.NewCounter(cv)
|
|
||||||
collectors = append(collectors, cv)
|
|
||||||
|
|
||||||
return &prom, collectors, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerCounterVec(cv *stdprometheus.CounterVec) (*stdprometheus.CounterVec, error) {
|
|
||||||
err := stdprometheus.Register(cv)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
e, ok := err.(stdprometheus.AlreadyRegisteredError)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error registering CounterVec: %s", e)
|
|
||||||
}
|
|
||||||
cv = e.ExistingCollector.(*stdprometheus.CounterVec)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerHistogramVec(hv *stdprometheus.HistogramVec) (*stdprometheus.HistogramVec, error) {
|
|
||||||
err := stdprometheus.Register(hv)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
e, ok := err.(stdprometheus.AlreadyRegisteredError)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error registering HistogramVec: %s", e)
|
|
||||||
}
|
|
||||||
hv = e.ExistingCollector.(*stdprometheus.HistogramVec)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hv, nil
|
|
||||||
}
|
|
|
@ -1,189 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/testhelpers"
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
dto "github.com/prometheus/client_model/go"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/urfave/negroni"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPrometheus(t *testing.T) {
|
|
||||||
defer resetPrometheusValues()
|
|
||||||
|
|
||||||
metricsFamilies, err := prometheus.DefaultGatherer.Gather()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not gather metrics family: %s", err)
|
|
||||||
}
|
|
||||||
initialMetricsFamilyCount := len(metricsFamilies)
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
|
|
||||||
req1 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/ok", ioutil.NopCloser(nil))
|
|
||||||
req2 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/metrics", ioutil.NopCloser(nil))
|
|
||||||
|
|
||||||
httpHandler := setupTestHTTPHandler()
|
|
||||||
httpHandler.ServeHTTP(recorder, req1)
|
|
||||||
httpHandler.ServeHTTP(recorder, req2)
|
|
||||||
|
|
||||||
body := recorder.Body.String()
|
|
||||||
if !strings.Contains(body, reqsTotalName) {
|
|
||||||
t.Errorf("body does not contain request total entry '%s'", reqsTotalName)
|
|
||||||
}
|
|
||||||
if !strings.Contains(body, reqDurationName) {
|
|
||||||
t.Errorf("body does not contain request duration entry '%s'", reqDurationName)
|
|
||||||
}
|
|
||||||
if !strings.Contains(body, retriesTotalName) {
|
|
||||||
t.Errorf("body does not contain total retries entry '%s'", retriesTotalName)
|
|
||||||
}
|
|
||||||
|
|
||||||
metricsFamilies, err = prometheus.DefaultGatherer.Gather()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not gather metrics families: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
labels map[string]string
|
|
||||||
assert func(*dto.MetricFamily)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: reqsTotalName,
|
|
||||||
labels: map[string]string{
|
|
||||||
"code": "200",
|
|
||||||
"method": http.MethodGet,
|
|
||||||
"service": "test",
|
|
||||||
},
|
|
||||||
assert: func(family *dto.MetricFamily) {
|
|
||||||
cv := uint(family.Metric[0].Counter.GetValue())
|
|
||||||
expectedCv := uint(2)
|
|
||||||
if cv != expectedCv {
|
|
||||||
t.Errorf("gathered metrics do not contain correct value for total requests, got %d expected %d", cv, expectedCv)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: reqDurationName,
|
|
||||||
labels: map[string]string{
|
|
||||||
"service": "test",
|
|
||||||
"code": "200",
|
|
||||||
},
|
|
||||||
assert: func(family *dto.MetricFamily) {
|
|
||||||
sc := family.Metric[0].Histogram.GetSampleCount()
|
|
||||||
expectedSc := uint64(2)
|
|
||||||
if sc != expectedSc {
|
|
||||||
t.Errorf("gathered metrics do not contain correct sample count for request duration, got %d expected %d", sc, expectedSc)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: retriesTotalName,
|
|
||||||
labels: map[string]string{
|
|
||||||
"service": "test",
|
|
||||||
},
|
|
||||||
assert: func(family *dto.MetricFamily) {
|
|
||||||
cv := uint(family.Metric[0].Counter.GetValue())
|
|
||||||
expectedCv := uint(1)
|
|
||||||
if cv != expectedCv {
|
|
||||||
t.Errorf("gathered metrics do not contain correct value for total retries, got '%d' expected '%d'", cv, expectedCv)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, len(tests), len(metricsFamilies)-initialMetricsFamilyCount, "gathered Traefik metrics count does not match tests count")
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
family := findMetricFamily(test.name, metricsFamilies)
|
|
||||||
if family == nil {
|
|
||||||
t.Errorf("gathered metrics do not contain '%s'", test.name)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, label := range family.Metric[0].Label {
|
|
||||||
val, ok := test.labels[*label.Name]
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("'%s' metric contains unexpected label '%s'", test.name, label)
|
|
||||||
} else if val != *label.Value {
|
|
||||||
t.Errorf("label '%s' in metric '%s' has wrong value '%s'", label, test.name, *label.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
test.assert(family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPrometheusRegisterMetricsMultipleTimes(t *testing.T) {
|
|
||||||
defer resetPrometheusValues()
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
req1 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/ok", ioutil.NopCloser(nil))
|
|
||||||
|
|
||||||
httpHandler := setupTestHTTPHandler()
|
|
||||||
httpHandler.ServeHTTP(recorder, req1)
|
|
||||||
|
|
||||||
httpHandler = setupTestHTTPHandler()
|
|
||||||
httpHandler.ServeHTTP(recorder, req1)
|
|
||||||
|
|
||||||
metricsFamilies, err := prometheus.DefaultGatherer.Gather()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not gather metrics families: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reqsTotalFamily := findMetricFamily(reqsTotalName, metricsFamilies)
|
|
||||||
if reqsTotalFamily == nil {
|
|
||||||
t.Fatalf("gathered metrics do not contain '%s'", reqsTotalName)
|
|
||||||
}
|
|
||||||
|
|
||||||
cv := uint(reqsTotalFamily.Metric[0].Counter.GetValue())
|
|
||||||
expectedCv := uint(2)
|
|
||||||
if cv != expectedCv {
|
|
||||||
t.Errorf("wrong counter value when registering metrics multiple times, got '%d' expected '%d'", cv, expectedCv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTestHTTPHandler() http.Handler {
|
|
||||||
serveMux := http.NewServeMux()
|
|
||||||
serveMux.Handle("/metrics", promhttp.Handler())
|
|
||||||
serveMux.Handle("/ok", &networkFailingHTTPHandler{failAtCalls: []int{1}, netErrorRecorder: &DefaultNetErrorRecorder{}})
|
|
||||||
|
|
||||||
metrics, _ := newPrometheusMetrics()
|
|
||||||
|
|
||||||
n := negroni.New()
|
|
||||||
n.Use(NewMetricsWrapper(metrics))
|
|
||||||
n.UseHandler(NewRetry(2, serveMux, NewMetricsRetryListener(metrics)))
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func resetPrometheusValues() {
|
|
||||||
_, collectors := newPrometheusMetrics()
|
|
||||||
|
|
||||||
for _, collector := range collectors {
|
|
||||||
prometheus.Unregister(collector)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPrometheusMetrics() (*Prometheus, []prometheus.Collector) {
|
|
||||||
prom, collectors, err := NewPrometheus("test", &types.Prometheus{})
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("Error creating Prometheus Metrics: %s", err))
|
|
||||||
}
|
|
||||||
return prom, collectors
|
|
||||||
}
|
|
||||||
|
|
||||||
func findMetricFamily(name string, families []*dto.MetricFamily) *dto.MetricFamily {
|
|
||||||
for _, family := range families {
|
|
||||||
if family.GetName() == name {
|
|
||||||
return family
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
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"
|
|
||||||
"github.com/go-kit/kit/metrics/statsd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Metrics = (*Statsd)(nil)
|
|
||||||
var _ RetryMetrics = (*Statsd)(nil)
|
|
||||||
|
|
||||||
var statsdClient = statsd.New("traefik.", kitlog.LoggerFunc(func(keyvals ...interface{}) error {
|
|
||||||
log.Info(keyvals)
|
|
||||||
return nil
|
|
||||||
}))
|
|
||||||
var statsdTicker *time.Ticker
|
|
||||||
|
|
||||||
// Statsd is an Implementation for Metrics that exposes statsd metrics for the latency
|
|
||||||
// and the number of requests partitioned by status code and method.
|
|
||||||
// - number of requests partitioned by status code and method
|
|
||||||
// - request durations
|
|
||||||
// - amount of retries happened
|
|
||||||
type Statsd struct {
|
|
||||||
reqsCounter metrics.Counter
|
|
||||||
reqDurationHistogram metrics.Histogram
|
|
||||||
retryCounter metrics.Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Statsd) getReqsCounter() metrics.Counter {
|
|
||||||
return s.reqsCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Statsd) getReqDurationHistogram() metrics.Histogram {
|
|
||||||
return s.reqDurationHistogram
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Statsd) getRetryCounter() metrics.Counter {
|
|
||||||
return s.retryCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStatsD creates new instance of StatsD
|
|
||||||
func NewStatsD(name string) *Statsd {
|
|
||||||
var m Statsd
|
|
||||||
|
|
||||||
m.reqsCounter = statsdClient.NewCounter(ddMetricsReqsName, 1.0).With("service", name)
|
|
||||||
m.reqDurationHistogram = statsdClient.NewTiming(ddMetricsLatencyName, 1.0).With("service", name)
|
|
||||||
m.retryCounter = statsdClient.NewCounter(ddRetriesTotalName, 1.0).With("service", name)
|
|
||||||
|
|
||||||
return &m
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitStatsdClient initializes metrics pusher and creates a statsdClient if not created already
|
|
||||||
func InitStatsdClient(config *types.Statsd) *time.Ticker {
|
|
||||||
if statsdTicker == nil {
|
|
||||||
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() {
|
|
||||||
statsdClient.SendLoop(report.C, "udp", address)
|
|
||||||
})
|
|
||||||
|
|
||||||
statsdTicker = report
|
|
||||||
}
|
|
||||||
return statsdTicker
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopStatsdClient stops internal statsdTicker which controls the pushing of metrics to StatsD Agent and resets it to `nil`
|
|
||||||
func StopStatsdClient() {
|
|
||||||
if statsdTicker != nil {
|
|
||||||
statsdTicker.Stop()
|
|
||||||
}
|
|
||||||
statsdTicker = nil
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package middlewares
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/containous/traefik/testhelpers"
|
|
||||||
"github.com/containous/traefik/types"
|
|
||||||
"github.com/stvp/go-udp-testing"
|
|
||||||
"github.com/urfave/negroni"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStatsD(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
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
InitStatsdClient(&types.Statsd{":18125", "1s"})
|
|
||||||
|
|
||||||
n := negroni.New()
|
|
||||||
c := NewStatsD("test")
|
|
||||||
defer StopStatsdClient()
|
|
||||||
metricsMiddlewareBackend := NewMetricsWrapper(c)
|
|
||||||
|
|
||||||
n.Use(metricsMiddlewareBackend)
|
|
||||||
r := http.NewServeMux()
|
|
||||||
r.HandleFunc(`/ok`, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprintln(w, "ok")
|
|
||||||
})
|
|
||||||
r.HandleFunc(`/not-found`, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
fmt.Fprintln(w, "not-found")
|
|
||||||
})
|
|
||||||
n.UseHandler(r)
|
|
||||||
|
|
||||||
req1 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/ok", nil)
|
|
||||||
req2 := testhelpers.MustNewRequest(http.MethodGet, "http://localhost:3000/not-found", nil)
|
|
||||||
|
|
||||||
retryListener := NewMetricsRetryListener(c)
|
|
||||||
retryListener.Retried(1)
|
|
||||||
retryListener.Retried(2)
|
|
||||||
|
|
||||||
expected := []string{
|
|
||||||
// We are only validating counts, as it is nearly impossible to validate latency, since it varies every run
|
|
||||||
"traefik.requests.total:2.000000|c\n",
|
|
||||||
"traefik.backend.retries.total:2.000000|c\n",
|
|
||||||
"traefik.request.duration",
|
|
||||||
}
|
|
||||||
|
|
||||||
udp.ShouldReceiveAll(t, expected, func() {
|
|
||||||
n.ServeHTTP(recorder, req1)
|
|
||||||
n.ServeHTTP(recorder, req2)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/containous/traefik/cluster"
|
"github.com/containous/traefik/cluster"
|
||||||
"github.com/containous/traefik/healthcheck"
|
"github.com/containous/traefik/healthcheck"
|
||||||
"github.com/containous/traefik/log"
|
"github.com/containous/traefik/log"
|
||||||
|
"github.com/containous/traefik/metrics"
|
||||||
"github.com/containous/traefik/middlewares"
|
"github.com/containous/traefik/middlewares"
|
||||||
"github.com/containous/traefik/middlewares/accesslog"
|
"github.com/containous/traefik/middlewares/accesslog"
|
||||||
"github.com/containous/traefik/provider"
|
"github.com/containous/traefik/provider"
|
||||||
|
@ -53,6 +54,7 @@ type Server struct {
|
||||||
routinesPool *safe.Pool
|
routinesPool *safe.Pool
|
||||||
leadership *cluster.Leadership
|
leadership *cluster.Leadership
|
||||||
defaultForwardingRoundTripper http.RoundTripper
|
defaultForwardingRoundTripper http.RoundTripper
|
||||||
|
metricsRegistry metrics.Registry
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverEntryPoints map[string]*serverEntryPoint
|
type serverEntryPoints map[string]*serverEntryPoint
|
||||||
|
@ -87,6 +89,11 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server {
|
||||||
server.routinesPool = safe.NewPool(context.Background())
|
server.routinesPool = safe.NewPool(context.Background())
|
||||||
server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration)
|
server.defaultForwardingRoundTripper = createHTTPTransport(globalConfiguration)
|
||||||
|
|
||||||
|
server.metricsRegistry = metrics.NewVoidRegistry()
|
||||||
|
if globalConfiguration.Web != nil && globalConfiguration.Web.Metrics != nil {
|
||||||
|
server.registerMetricClients(globalConfiguration.Web.Metrics)
|
||||||
|
}
|
||||||
|
|
||||||
if globalConfiguration.Cluster != nil {
|
if globalConfiguration.Cluster != nil {
|
||||||
// leadership creation if cluster mode
|
// leadership creation if cluster mode
|
||||||
server.leadership = cluster.NewLeadership(server.routinesPool.Ctx(), globalConfiguration.Cluster)
|
server.leadership = cluster.NewLeadership(server.routinesPool.Ctx(), globalConfiguration.Cluster)
|
||||||
|
@ -215,7 +222,7 @@ func (server *Server) Close() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}(ctx)
|
}(ctx)
|
||||||
stopMetricsClients(server.globalConfiguration)
|
stopMetricsClients()
|
||||||
server.stopLeadership()
|
server.stopLeadership()
|
||||||
server.routinesPool.Cleanup()
|
server.routinesPool.Cleanup()
|
||||||
close(server.configurationChan)
|
close(server.configurationChan)
|
||||||
|
@ -253,14 +260,12 @@ func (server *Server) startHTTPServers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newServerEntryPoint *serverEntryPoint) *serverEntryPoint {
|
func (server *Server) setupServerEntryPoint(newServerEntryPointName string, newServerEntryPoint *serverEntryPoint) *serverEntryPoint {
|
||||||
serverMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler(), metrics}
|
serverMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler(), stats}
|
||||||
if server.accessLoggerMiddleware != nil {
|
if server.accessLoggerMiddleware != nil {
|
||||||
serverMiddlewares = append(serverMiddlewares, server.accessLoggerMiddleware)
|
serverMiddlewares = append(serverMiddlewares, server.accessLoggerMiddleware)
|
||||||
}
|
}
|
||||||
initializeMetricsClients(server.globalConfiguration)
|
if server.metricsRegistry.IsEnabled() {
|
||||||
metrics := newMetrics(server.globalConfiguration, newServerEntryPointName)
|
serverMiddlewares = append(serverMiddlewares, middlewares.NewMetricsWrapper(server.metricsRegistry, newServerEntryPointName))
|
||||||
if metrics != nil {
|
|
||||||
serverMiddlewares = append(serverMiddlewares, middlewares.NewMetricsWrapper(metrics))
|
|
||||||
}
|
}
|
||||||
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Statistics != nil {
|
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Statistics != nil {
|
||||||
statsRecorder = middlewares.NewStatsRecorder(server.globalConfiguration.Web.Statistics.RecentErrors)
|
statsRecorder = middlewares.NewStatsRecorder(server.globalConfiguration.Web.Statistics.RecentErrors)
|
||||||
|
@ -879,14 +884,12 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics := newMetrics(server.globalConfiguration, frontend.Backend)
|
|
||||||
|
|
||||||
if globalConfiguration.Retry != nil {
|
if globalConfiguration.Retry != nil {
|
||||||
retryListener := middlewares.NewMetricsRetryListener(metrics)
|
retryListener := middlewares.NewMetricsRetryListener(server.metricsRegistry, frontend.Backend)
|
||||||
lb = registerRetryMiddleware(lb, globalConfiguration, configuration, frontend.Backend, retryListener)
|
lb = registerRetryMiddleware(lb, globalConfiguration, configuration, frontend.Backend, retryListener)
|
||||||
}
|
}
|
||||||
if metrics != nil {
|
if server.metricsRegistry.IsEnabled() {
|
||||||
negroni.Use(middlewares.NewMetricsWrapper(metrics))
|
negroni.Use(middlewares.NewMetricsWrapper(server.metricsRegistry, frontend.Backend))
|
||||||
}
|
}
|
||||||
|
|
||||||
ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange)
|
ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange)
|
||||||
|
@ -1135,54 +1138,30 @@ func (*Server) configureBackends(backends map[string]*types.Backend) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMetrics instantiates the proper Metrics implementation, depending on the global configuration.
|
func (server *Server) registerMetricClients(metricsConfig *types.Metrics) {
|
||||||
// Note that given there is no metrics instrumentation configured, it will return nil.
|
registries := []metrics.Registry{}
|
||||||
func newMetrics(globalConfig GlobalConfiguration, name string) middlewares.Metrics {
|
|
||||||
metricsEnabled := globalConfig.Web != nil && globalConfig.Web.Metrics != nil
|
|
||||||
if metricsEnabled {
|
|
||||||
// Create MultiMetric
|
|
||||||
metrics := []middlewares.Metrics{}
|
|
||||||
|
|
||||||
if globalConfig.Web.Metrics.Prometheus != nil {
|
if metricsConfig.Prometheus != nil {
|
||||||
metric, _, err := middlewares.NewPrometheus(name, globalConfig.Web.Metrics.Prometheus)
|
registries = append(registries, metrics.RegisterPrometheus(metricsConfig.Prometheus))
|
||||||
if err != nil {
|
log.Debug("Configured Prometheus metrics")
|
||||||
log.Errorf("Error creating Prometheus metrics implementation: %s", err)
|
}
|
||||||
}
|
if metricsConfig.Datadog != nil {
|
||||||
log.Debug("Configured Prometheus metrics")
|
registries = append(registries, metrics.RegisterDatadog(metricsConfig.Datadog))
|
||||||
metrics = append(metrics, metric)
|
log.Debugf("Configured DataDog metrics pushing to %s once every %s", metricsConfig.Datadog.Address, metricsConfig.Datadog.PushInterval)
|
||||||
}
|
}
|
||||||
if globalConfig.Web.Metrics.Datadog != nil {
|
if metricsConfig.StatsD != nil {
|
||||||
metric := middlewares.NewDataDog(name)
|
registries = append(registries, metrics.RegisterStatsd(metricsConfig.StatsD))
|
||||||
log.Debugf("Configured DataDog metrics pushing to %s once every %s", globalConfig.Web.Metrics.Datadog.Address, globalConfig.Web.Metrics.Datadog.PushInterval)
|
log.Debugf("Configured StatsD metrics pushing to %s once every %s", metricsConfig.StatsD.Address, metricsConfig.StatsD.PushInterval)
|
||||||
metrics = append(metrics, metric)
|
|
||||||
}
|
|
||||||
if globalConfig.Web.Metrics.StatsD != nil {
|
|
||||||
metric := middlewares.NewStatsD(name)
|
|
||||||
log.Debugf("Configured StatsD metrics pushing to %s once every %s", globalConfig.Web.Metrics.StatsD.Address, globalConfig.Web.Metrics.StatsD.PushInterval)
|
|
||||||
metrics = append(metrics, metric)
|
|
||||||
}
|
|
||||||
|
|
||||||
return middlewares.NewMultiMetrics(metrics)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if len(registries) > 0 {
|
||||||
}
|
server.metricsRegistry = metrics.NewMultiRegistry(registries)
|
||||||
|
|
||||||
func initializeMetricsClients(globalConfig GlobalConfiguration) {
|
|
||||||
metricsEnabled := globalConfig.Web != nil && globalConfig.Web.Metrics != nil
|
|
||||||
if metricsEnabled {
|
|
||||||
if globalConfig.Web.Metrics.Datadog != nil {
|
|
||||||
middlewares.InitDatadogClient(globalConfig.Web.Metrics.Datadog)
|
|
||||||
}
|
|
||||||
if globalConfig.Web.Metrics.StatsD != nil {
|
|
||||||
middlewares.InitStatsdClient(globalConfig.Web.Metrics.StatsD)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopMetricsClients(globalConfig GlobalConfiguration) {
|
func stopMetricsClients() {
|
||||||
middlewares.StopDatadogClient()
|
metrics.StopDatadog()
|
||||||
middlewares.StopStatsdClient()
|
metrics.StopStatsd()
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerRetryMiddleware(
|
func registerRetryMiddleware(
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/containous/flaeg"
|
"github.com/containous/flaeg"
|
||||||
"github.com/containous/mux"
|
"github.com/containous/mux"
|
||||||
"github.com/containous/traefik/healthcheck"
|
"github.com/containous/traefik/healthcheck"
|
||||||
|
"github.com/containous/traefik/metrics"
|
||||||
"github.com/containous/traefik/middlewares"
|
"github.com/containous/traefik/middlewares"
|
||||||
"github.com/containous/traefik/testhelpers"
|
"github.com/containous/traefik/testhelpers"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
|
@ -493,44 +494,6 @@ func TestConfigureBackends(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewMetrics(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
desc string
|
|
||||||
globalConfig GlobalConfiguration
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "metrics disabled",
|
|
||||||
globalConfig: GlobalConfiguration{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "prometheus metrics",
|
|
||||||
globalConfig: GlobalConfiguration{
|
|
||||||
Web: &WebProvider{
|
|
||||||
Metrics: &types.Metrics{
|
|
||||||
Prometheus: &types.Prometheus{
|
|
||||||
Buckets: types.Buckets{0.1, 0.3, 1.2, 5.0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
metricsImpl := newMetrics(tc.globalConfig, "test1")
|
|
||||||
if metricsImpl != nil {
|
|
||||||
if _, ok := metricsImpl.(*middlewares.MultiMetrics); !ok {
|
|
||||||
t.Errorf("invalid metricsImpl type, got %T want %T", metricsImpl, &middlewares.MultiMetrics{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegisterRetryMiddleware(t *testing.T) {
|
func TestRegisterRetryMiddleware(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -635,6 +598,7 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) {
|
||||||
"test": test.entrypoint,
|
"test": test.entrypoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metricsRegistry: metrics.NewVoidRegistry(),
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.serverEntryPoints = srv.buildEntryPoints(srv.globalConfiguration)
|
srv.serverEntryPoints = srv.buildEntryPoints(srv.globalConfiguration)
|
||||||
|
|
|
@ -23,7 +23,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
metrics = thoas_stats.New()
|
stats = thoas_stats.New()
|
||||||
statsRecorder *middlewares.StatsRecorder
|
statsRecorder *middlewares.StatsRecorder
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ type healthResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *WebProvider) getHealthHandler(response http.ResponseWriter, request *http.Request) {
|
func (provider *WebProvider) getHealthHandler(response http.ResponseWriter, request *http.Request) {
|
||||||
health := &healthResponse{Data: metrics.Data()}
|
health := &healthResponse{Data: stats.Data()}
|
||||||
if statsRecorder != nil {
|
if statsRecorder != nil {
|
||||||
health.Stats = statsRecorder.Data()
|
health.Stats = statsRecorder.Data()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue