Skip to content

Commit

Permalink
Support for Metrics and Prometheus.
Browse files Browse the repository at this point in the history
  • Loading branch information
enxebre committed Jan 16, 2017
1 parent dd85cbc commit 175659a
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 5 deletions.
7 changes: 7 additions & 0 deletions configuration.go
Expand Up @@ -328,6 +328,13 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
RecentErrors: 10,
}

// default Metrics
defaultWeb.Metrics = &types.Metrics{
Prometheus: &types.Prometheus{
Buckets: types.Buckets{100, 300, 1200, 5000},
},
}

// default Marathon
var defaultMarathon provider.Marathon
defaultMarathon.Watch = true
Expand Down
5 changes: 5 additions & 0 deletions docs/toml.md
Expand Up @@ -715,6 +715,11 @@ $ curl -s "http://localhost:8080/api" | jq .
- `/api/providers/{provider}/frontends/{frontend}/routes`: `GET` routes in a frontend
- `/api/providers/{provider}/frontends/{frontend}/routes/{route}`: `GET` a route in a frontend
- `/metrics`: You can enable Traefik to export internal metrics to different monitoring systems (Only Prometheus is supported at the moment).
```bash
$ traefik --web.metrics.prometheus --web.metrics.prometheus.buckets="100,300"
```
## Docker backend
Expand Down
36 changes: 34 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion glide.yaml
Expand Up @@ -62,7 +62,7 @@ import:
subpackages:
- plugin/rewrite
- package: github.com/xenolf/lego
version: ce8fb060cb8361a9ff8b5fb7c2347fa907b6fcac
version: ce8fb060cb8361a9ff8b5fb7c2347fa907b6fcac
subpackages:
- acme
- package: golang.org/x/net
Expand Down Expand Up @@ -114,3 +114,7 @@ import:
- package: github.com/google/go-github
- package: github.com/hashicorp/go-version
- package: github.com/mvdan/xurls
- package: github.com/go-kit/kit
version: v0.3.0
subpackages:
- metrics
51 changes: 51 additions & 0 deletions middlewares/metrics.go
@@ -0,0 +1,51 @@
package middlewares

import (
"github.com/go-kit/kit/metrics"
"net/http"
"strconv"
"time"
)

// Metrics is an Interface that must be satisfied by any system that
// wants to expose and monitor metrics
type Metrics interface {
getReqsCounter() metrics.Counter
getLatencyHistogram() metrics.Histogram
handler() http.Handler
}

// MetricsWrapper is a Negroni compatible Handler which relies on a
// given Metrics implementation to expose and monitor Traefik metrics
type MetricsWrapper struct {
Impl Metrics
}

// NewMetricsWrapper return a MetricsWrapper struct with
// a given Metrics implementation e.g Prometheuss
func NewMetricsWrapper(impl Metrics) *MetricsWrapper {
var metricsWrapper = MetricsWrapper{
Impl: impl,
}

return &metricsWrapper
}

func (m *MetricsWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
start := time.Now()
prw := &responseRecorder{rw, http.StatusOK}
next(prw, r)
labels := []string{"code", strconv.Itoa(prw.StatusCode()), "method", r.Method}
m.Impl.getReqsCounter().With(labels...).Add(1)
m.Impl.getLatencyHistogram().With(labels...).Observe(float64(time.Since(start).Nanoseconds()) / 1000000)
}

func (rw *responseRecorder) StatusCode() int {
return rw.statusCode
}

// Handler is the chance for the Metrics implementation
// to expose its metrics on a server endpoint
func (m *MetricsWrapper) Handler() http.Handler {
return m.Impl.handler()
}
65 changes: 65 additions & 0 deletions middlewares/prometheus.go
@@ -0,0 +1,65 @@
package middlewares

import (
"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"
"github.com/prometheus/client_golang/prometheus/promhttp"
"net/http"
)

const (
reqsName = "requests_total"
latencyName = "request_duration_milliseconds"
)

// Prometheus is an Implementation for Metrics that exposes prometheus metrics for the number of requests,
// the latency and the response size, partitioned by status code and method.
type Prometheus struct {
reqsCounter metrics.Counter
latencyHistogram metrics.Histogram
}

func (p *Prometheus) getReqsCounter() metrics.Counter {
return p.reqsCounter
}

func (p *Prometheus) getLatencyHistogram() metrics.Histogram {
return p.latencyHistogram
}

// NewPrometheus returns a new prometheus Metrics implementation.
func NewPrometheus(name string, config *types.Prometheus) *Prometheus {
var m Prometheus
m.reqsCounter = prometheus.NewCounterFrom(
stdprometheus.CounterOpts{
Name: reqsName,
Help: "How many HTTP requests processed, partitioned by status code and method.",
ConstLabels: stdprometheus.Labels{"service": name},
},
[]string{"code", "method"},
)

var buckets []float64
if config.Buckets != nil {
buckets = config.Buckets
} else {
buckets = []float64{100, 300, 1200, 5000}
}

m.latencyHistogram = prometheus.NewHistogramFrom(
stdprometheus.HistogramOpts{
Name: latencyName,
Help: "How long it took to process the request, partitioned by status code and method.",
ConstLabels: stdprometheus.Labels{"service": name},
Buckets: buckets,
},
[]string{"code", "method"},
)
return &m
}

func (p *Prometheus) handler() http.Handler {
return promhttp.Handler()
}
47 changes: 47 additions & 0 deletions middlewares/prometheus_test.go
@@ -0,0 +1,47 @@
package middlewares

import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/codegangsta/negroni"
"github.com/containous/traefik/types"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

func TestPrometheus(t *testing.T) {
recorder := httptest.NewRecorder()

n := negroni.New()
metricsMiddlewareBackend := NewMetricsWrapper(NewPrometheus("test", &types.Prometheus{}))
n.Use(metricsMiddlewareBackend)
r := http.NewServeMux()
r.Handle("/metrics", promhttp.Handler())
r.HandleFunc(`/ok`, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
n.UseHandler(r)

req1, err := http.NewRequest("GET", "http://localhost:3000/ok", nil)
if err != nil {
t.Error(err)
}
req2, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
if err != nil {
t.Error(err)
}

n.ServeHTTP(recorder, req1)
n.ServeHTTP(recorder, req2)
body := recorder.Body.String()
if !strings.Contains(body, reqsName) {
t.Errorf("body does not contain request total entry '%s'", reqsName)
}
if !strings.Contains(body, latencyName) {
t.Errorf("body does not contain request duration entry '%s'", reqsName)
}
}
13 changes: 13 additions & 0 deletions server.go
Expand Up @@ -167,8 +167,15 @@ func (server *Server) stopLeadership() {

func (server *Server) startHTTPServers() {
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)

for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
serverMiddlewares := []negroni.Handler{server.loggerMiddleware, metrics}
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Metrics != nil {
if server.globalConfiguration.Web.Metrics.Prometheus != nil {
metricsMiddleware := middlewares.NewMetricsWrapper(middlewares.NewPrometheus("Global", server.globalConfiguration.Web.Metrics.Prometheus))
serverMiddlewares = append(serverMiddlewares, metricsMiddleware)
}
}
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Statistics != nil {
statsRecorder = middlewares.NewStatsRecorder(server.globalConfiguration.Web.Statistics.RecentErrors)
serverMiddlewares = append(serverMiddlewares, statsRecorder)
Expand Down Expand Up @@ -691,6 +698,12 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
}

var negroni = negroni.New()
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Metrics != nil {
if server.globalConfiguration.Web.Metrics.Prometheus != nil {
metricsMiddlewareBackend := middlewares.NewMetricsWrapper(middlewares.NewPrometheus(frontend.Backend, server.globalConfiguration.Web.Metrics.Prometheus))
negroni.Use(metricsMiddlewareBackend)
}
}
if configuration.Backends[frontend.Backend].CircuitBreaker != nil {
log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression)
cbreaker, err := middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger))
Expand Down
1 change: 1 addition & 0 deletions traefik.go
Expand Up @@ -106,6 +106,7 @@ Complete documentation is available at https://traefik.io`,
f.AddParser(reflect.TypeOf(types.Constraints{}), &types.Constraints{})
f.AddParser(reflect.TypeOf(k8s.Namespaces{}), &k8s.Namespaces{})
f.AddParser(reflect.TypeOf([]acme.Domain{}), &acme.Domains{})
f.AddParser(reflect.TypeOf(types.Buckets{}), &types.Buckets{})

//add commands
f.AddCommand(cmd.NewVersionCmd())
Expand Down
46 changes: 44 additions & 2 deletions types/types.go
Expand Up @@ -4,10 +4,10 @@ import (
"encoding"
"errors"
"fmt"
"strings"

"github.com/docker/libkv/store"
"github.com/ryanuber/go-glob"
"strconv"
"strings"
)

// Backend holds backend configuration.
Expand Down Expand Up @@ -250,3 +250,45 @@ func CanonicalDomain(domain string) string {
type Statistics struct {
RecentErrors int `description:"Number of recent errors logged"`
}

// Metrics provides options to expose and send Traefik metrics to different third party monitoring systems
type Metrics struct {
Prometheus *Prometheus `description:"Prometheus metrics exporter type"`
}

// Prometheus can contain specific configuration used by the Prometheus Metrics exporter
type Prometheus struct {
Buckets Buckets `description:"Buckets for latency metrics"`
}

// Buckets holds Prometheus Buckets
type Buckets []float64

//Set adds strings elem into the the parser
//it splits str on "," and ";" and apply ParseFloat to string
func (b *Buckets) Set(str string) error {
fargs := func(c rune) bool {
return c == ',' || c == ';'
}
// get function
slice := strings.FieldsFunc(str, fargs)
for _, bucket := range slice {
bu, err := strconv.ParseFloat(bucket, 64)
if err != nil {
return err
}
*b = append(*b, bu)
}
return nil
}

//Get []float64
func (b *Buckets) Get() interface{} { return Buckets(*b) }

//String return slice in a string
func (b *Buckets) String() string { return fmt.Sprintf("%v", *b) }

//SetValue sets []float64 into the parser
func (b *Buckets) SetValue(val interface{}) {
*b = Buckets(val.(Buckets))
}

0 comments on commit 175659a

Please sign in to comment.