2017-01-12 13:34:54 +00:00
|
|
|
package middlewares
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
2018-01-26 10:58:03 +00:00
|
|
|
"strings"
|
|
|
|
"sync/atomic"
|
2017-01-12 13:34:54 +00:00
|
|
|
"time"
|
2017-09-08 09:22:03 +00:00
|
|
|
"unicode/utf8"
|
2017-04-18 06:22:06 +00:00
|
|
|
|
2017-09-08 09:22:03 +00:00
|
|
|
"github.com/containous/traefik/log"
|
2017-08-23 18:46:03 +00:00
|
|
|
"github.com/containous/traefik/metrics"
|
|
|
|
gokitmetrics "github.com/go-kit/kit/metrics"
|
2018-01-26 10:58:03 +00:00
|
|
|
"github.com/urfave/negroni"
|
2017-01-12 13:34:54 +00:00
|
|
|
)
|
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
const (
|
|
|
|
protoHTTP = "http"
|
|
|
|
protoSSE = "sse"
|
|
|
|
protoWebsocket = "websocket"
|
|
|
|
)
|
|
|
|
|
|
|
|
// NewEntryPointMetricsMiddleware creates a new metrics middleware for an Entrypoint.
|
|
|
|
func NewEntryPointMetricsMiddleware(registry metrics.Registry, entryPointName string) negroni.Handler {
|
|
|
|
return &metricsMiddleware{
|
|
|
|
reqsCounter: registry.EntrypointReqsCounter(),
|
|
|
|
reqDurationHistogram: registry.EntrypointReqDurationHistogram(),
|
|
|
|
openConnsGauge: registry.EntrypointOpenConnsGauge(),
|
|
|
|
baseLabels: []string{"entrypoint", entryPointName},
|
|
|
|
}
|
2017-01-12 13:34:54 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
// NewBackendMetricsMiddleware creates a new metrics middleware for a Backend.
|
|
|
|
func NewBackendMetricsMiddleware(registry metrics.Registry, backendName string) negroni.Handler {
|
|
|
|
return &metricsMiddleware{
|
|
|
|
reqsCounter: registry.BackendReqsCounter(),
|
|
|
|
reqDurationHistogram: registry.BackendReqDurationHistogram(),
|
|
|
|
openConnsGauge: registry.BackendOpenConnsGauge(),
|
|
|
|
baseLabels: []string{"backend", backendName},
|
2017-01-12 13:34:54 +00:00
|
|
|
}
|
2018-01-26 10:58:03 +00:00
|
|
|
}
|
2017-01-12 13:34:54 +00:00
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
type metricsMiddleware struct {
|
2018-04-17 08:32:03 +00:00
|
|
|
// 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
|
2018-01-26 10:58:03 +00:00
|
|
|
reqsCounter gokitmetrics.Counter
|
|
|
|
reqDurationHistogram gokitmetrics.Histogram
|
|
|
|
openConnsGauge gokitmetrics.Gauge
|
|
|
|
baseLabels []string
|
2017-01-12 13:34:54 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
|
|
|
labels := []string{"method", getMethod(r), "protocol", getRequestProtocol(r)}
|
|
|
|
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)
|
|
|
|
|
2017-01-12 13:34:54 +00:00
|
|
|
start := time.Now()
|
2018-01-26 10:58:03 +00:00
|
|
|
recorder := &responseRecorder{rw, http.StatusOK}
|
|
|
|
next(recorder, r)
|
2017-06-15 14:06:02 +00:00
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
labels = append(labels, "code", strconv.Itoa(recorder.statusCode))
|
|
|
|
m.reqsCounter.With(labels...).Add(1)
|
2018-02-13 16:14:04 +00:00
|
|
|
m.reqDurationHistogram.With(labels...).Observe(time.Since(start).Seconds())
|
2018-01-26 10:58:03 +00:00
|
|
|
}
|
2017-06-15 14:06:02 +00:00
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
func getRequestProtocol(req *http.Request) string {
|
|
|
|
switch {
|
|
|
|
case isWebsocketRequest(req):
|
|
|
|
return protoWebsocket
|
|
|
|
case isSSERequest(req):
|
|
|
|
return protoSSE
|
|
|
|
default:
|
|
|
|
return protoHTTP
|
|
|
|
}
|
2017-08-23 18:46:03 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
// 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")
|
2017-08-23 18:46:03 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
// 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
|
2017-04-18 06:22:06 +00:00
|
|
|
}
|
|
|
|
|
2017-09-08 09:22:03 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-01-26 10:58:03 +00:00
|
|
|
type retryMetrics interface {
|
|
|
|
BackendRetriesCounter() gokitmetrics.Counter
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewMetricsRetryListener instantiates a MetricsRetryListener with the given retryMetrics.
|
|
|
|
func NewMetricsRetryListener(retryMetrics retryMetrics, backendName string) RetryListener {
|
|
|
|
return &MetricsRetryListener{retryMetrics: retryMetrics, backendName: backendName}
|
|
|
|
}
|
|
|
|
|
2017-04-18 06:22:06 +00:00
|
|
|
// MetricsRetryListener is an implementation of the RetryListener interface to
|
2017-08-23 18:46:03 +00:00
|
|
|
// record RequestMetrics about retry attempts.
|
2017-04-18 06:22:06 +00:00
|
|
|
type MetricsRetryListener struct {
|
2017-08-23 18:46:03 +00:00
|
|
|
retryMetrics retryMetrics
|
|
|
|
backendName string
|
2017-01-12 13:34:54 +00:00
|
|
|
}
|
|
|
|
|
2017-08-23 18:46:03 +00:00
|
|
|
// Retried tracks the retry in the RequestMetrics implementation.
|
2017-08-28 10:50:02 +00:00
|
|
|
func (m *MetricsRetryListener) Retried(req *http.Request, attempt int) {
|
2018-01-26 10:58:03 +00:00
|
|
|
m.retryMetrics.BackendRetriesCounter().With("backend", m.backendName).Add(1)
|
2017-01-12 13:34:54 +00:00
|
|
|
}
|