1
0
Fork 0

Add Metrics

This commit is contained in:
Michael 2019-07-18 21:36:05 +02:00 committed by Traefiker Bot
parent 4dc448056c
commit 8e97af8dc3
121 changed files with 8364 additions and 3811 deletions

View file

@ -0,0 +1,158 @@
package metrics
import (
"context"
"net/http"
"strconv"
"strings"
"sync/atomic"
"time"
"unicode/utf8"
"github.com/containous/alice"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/metrics"
"github.com/containous/traefik/pkg/middlewares"
"github.com/containous/traefik/pkg/middlewares/retry"
gokitmetrics "github.com/go-kit/kit/metrics"
)
const (
protoHTTP = "http"
protoSSE = "sse"
protoWebsocket = "websocket"
typeName = "Metrics"
nameEntrypoint = "metrics-entrypoint"
nameService = "metrics-service"
)
type metricsMiddleware struct {
// Important: Since this int64 field is using sync/atomic, it has to be at the top of the struct due to a bug on 32-bit platform
// See: https://golang.org/pkg/sync/atomic/ for more information
openConns int64
next http.Handler
reqsCounter gokitmetrics.Counter
reqDurationHistogram gokitmetrics.Histogram
openConnsGauge gokitmetrics.Gauge
baseLabels []string
}
// NewEntryPointMiddleware creates a new metrics middleware for an Entrypoint.
func NewEntryPointMiddleware(ctx context.Context, next http.Handler, registry metrics.Registry, entryPointName string) http.Handler {
middlewares.GetLogger(ctx, nameEntrypoint, typeName).Debug("Creating middleware")
return &metricsMiddleware{
next: next,
reqsCounter: registry.EntryPointReqsCounter(),
reqDurationHistogram: registry.EntryPointReqDurationHistogram(),
openConnsGauge: registry.EntryPointOpenConnsGauge(),
baseLabels: []string{"entrypoint", entryPointName},
}
}
// NewServiceMiddleware creates a new metrics middleware for a Service.
func NewServiceMiddleware(ctx context.Context, next http.Handler, registry metrics.Registry, serviceName string) http.Handler {
middlewares.GetLogger(ctx, nameService, typeName).Debug("Creating middleware")
return &metricsMiddleware{
next: next,
reqsCounter: registry.ServiceReqsCounter(),
reqDurationHistogram: registry.ServiceReqDurationHistogram(),
openConnsGauge: registry.ServiceOpenConnsGauge(),
baseLabels: []string{"service", serviceName},
}
}
// WrapEntryPointHandler Wraps metrics entrypoint to alice.Constructor.
func WrapEntryPointHandler(ctx context.Context, registry metrics.Registry, entryPointName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return NewEntryPointMiddleware(ctx, next, registry, entryPointName), nil
}
}
// WrapServiceHandler Wraps metrics service to alice.Constructor.
func WrapServiceHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return NewServiceMiddleware(ctx, next, registry, serviceName), nil
}
}
func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
labels := []string{"method", getMethod(req), "protocol", getRequestProtocol(req)}
labels = append(labels, m.baseLabels...)
openConns := atomic.AddInt64(&m.openConns, 1)
m.openConnsGauge.With(labels...).Set(float64(openConns))
defer func(labelValues []string) {
openConns := atomic.AddInt64(&m.openConns, -1)
m.openConnsGauge.With(labelValues...).Set(float64(openConns))
}(labels)
start := time.Now()
recorder := &responseRecorder{rw, http.StatusOK}
m.next.ServeHTTP(recorder, req)
labels = append(labels, "code", strconv.Itoa(recorder.statusCode))
m.reqsCounter.With(labels...).Add(1)
m.reqDurationHistogram.With(labels...).Observe(time.Since(start).Seconds())
}
func getRequestProtocol(req *http.Request) string {
switch {
case isWebsocketRequest(req):
return protoWebsocket
case isSSERequest(req):
return protoSSE
default:
return protoHTTP
}
}
// isWebsocketRequest determines if the specified HTTP request is a websocket handshake request.
func isWebsocketRequest(req *http.Request) bool {
return containsHeader(req, "Connection", "upgrade") && containsHeader(req, "Upgrade", "websocket")
}
// isSSERequest determines if the specified HTTP request is a request for an event subscription.
func isSSERequest(req *http.Request) bool {
return containsHeader(req, "Accept", "text/event-stream")
}
func containsHeader(req *http.Request, name, value string) bool {
items := strings.Split(req.Header.Get(name), ",")
for _, item := range items {
if value == strings.ToLower(strings.TrimSpace(item)) {
return true
}
}
return false
}
func getMethod(r *http.Request) string {
if !utf8.ValidString(r.Method) {
log.Warnf("Invalid HTTP method encoding: %s", r.Method)
return "NON_UTF8_HTTP_METHOD"
}
return r.Method
}
type retryMetrics interface {
ServiceRetriesCounter() gokitmetrics.Counter
}
// NewRetryListener instantiates a MetricsRetryListener with the given retryMetrics.
func NewRetryListener(retryMetrics retryMetrics, serviceName string) retry.Listener {
return &RetryListener{retryMetrics: retryMetrics, serviceName: serviceName}
}
// RetryListener is an implementation of the RetryListener interface to
// record RequestMetrics about retry attempts.
type RetryListener struct {
retryMetrics retryMetrics
serviceName string
}
// Retried tracks the retry in the RequestMetrics implementation.
func (m *RetryListener) Retried(req *http.Request, attempt int) {
m.retryMetrics.ServiceRetriesCounter().With("service", m.serviceName).Add(1)
}

View file

@ -0,0 +1,58 @@
package metrics
import (
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/go-kit/kit/metrics"
)
// CollectingCounter is a metrics.Counter implementation that enables access to the CounterValue and LastLabelValues.
type CollectingCounter struct {
CounterValue float64
LastLabelValues []string
}
// With is there to satisfy the metrics.Counter interface.
func (c *CollectingCounter) With(labelValues ...string) metrics.Counter {
c.LastLabelValues = labelValues
return c
}
// Add is there to satisfy the metrics.Counter interface.
func (c *CollectingCounter) Add(delta float64) {
c.CounterValue += delta
}
func TestMetricsRetryListener(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
retryMetrics := newCollectingRetryMetrics()
retryListener := NewRetryListener(retryMetrics, "serviceName")
retryListener.Retried(req, 1)
retryListener.Retried(req, 2)
wantCounterValue := float64(2)
if retryMetrics.retriesCounter.CounterValue != wantCounterValue {
t.Errorf("got counter value of %f, want %f", retryMetrics.retriesCounter.CounterValue, wantCounterValue)
}
wantLabelValues := []string{"service", "serviceName"}
if !reflect.DeepEqual(retryMetrics.retriesCounter.LastLabelValues, wantLabelValues) {
t.Errorf("wrong label values %v used, want %v", retryMetrics.retriesCounter.LastLabelValues, wantLabelValues)
}
}
// collectingRetryMetrics is an implementation of the retryMetrics interface that can be used inside tests to collect the times Add() was called.
type collectingRetryMetrics struct {
retriesCounter *CollectingCounter
}
func newCollectingRetryMetrics() *collectingRetryMetrics {
return &collectingRetryMetrics{retriesCounter: &CollectingCounter{}}
}
func (m *collectingRetryMetrics) ServiceRetriesCounter() metrics.Counter {
return m.retriesCounter
}

View file

@ -0,0 +1,37 @@
package metrics
import (
"bufio"
"net"
"net/http"
)
// responseRecorder captures information from the response and preserves it for
// later analysis.
type responseRecorder struct {
http.ResponseWriter
statusCode int
}
// WriteHeader captures the status code for later retrieval.
func (r *responseRecorder) WriteHeader(status int) {
r.ResponseWriter.WriteHeader(status)
r.statusCode = status
}
// Hijack hijacks the connection
func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.ResponseWriter.(http.Hijacker).Hijack()
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone
// away.
func (r *responseRecorder) CloseNotify() <-chan bool {
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Flush sends any buffered data to the client.
func (r *responseRecorder) Flush() {
r.ResponseWriter.(http.Flusher).Flush()
}