Merge v2.1 into master.

This commit is contained in:
Fernandez Ludovic 2020-03-05 16:07:04 +01:00
commit 09c07f45ee
18 changed files with 386 additions and 82 deletions

View file

@ -191,7 +191,7 @@ Use the `HTTP-01` challenge to generate and renew ACME certificates by provision
As described on the Let's Encrypt [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72), As described on the Let's Encrypt [community forum](https://community.letsencrypt.org/t/support-for-ports-other-than-80-and-443/3419/72),
when using the `HTTP-01` challenge, `certificatesResolvers.myresolver.acme.httpChallenge.entryPoint` must be reachable by Let's Encrypt through port 80. when using the `HTTP-01` challenge, `certificatesResolvers.myresolver.acme.httpChallenge.entryPoint` must be reachable by Let's Encrypt through port 80.
??? example "Using an EntryPoint Called http for the `httpChallenge`" ??? example "Using an EntryPoint Called web for the `httpChallenge`"
```toml tab="File (TOML)" ```toml tab="File (TOML)"
[entryPoints] [entryPoints]
@ -396,6 +396,13 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
### `caServer` ### `caServer`
_Required, Default="https://acme-v02.api.letsencrypt.org/directory"_
The CA server to use:
- Let's Encrypt production server: https://acme-v02.api.letsencrypt.org/directory
- Let's Encrypt staging server: https://acme-staging-v02.api.letsencrypt.org/directory
??? example "Using the Let's Encrypt staging server" ??? example "Using the Let's Encrypt staging server"
```toml tab="File (TOML)" ```toml tab="File (TOML)"
@ -422,6 +429,8 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
### `storage` ### `storage`
_Required, Default="acme.json"_
The `storage` option sets the location where your ACME certificates are saved to. The `storage` option sets the location where your ACME certificates are saved to.
```toml tab="File (TOML)" ```toml tab="File (TOML)"
@ -446,13 +455,7 @@ certificatesResolvers:
# ... # ...
``` ```
The value can refer to some kinds of storage: ACME certificates are stored in a JSON file that needs to have a `600` file mode.
- a JSON file
#### In a File
ACME certificates can be stored in a JSON file that needs to have a `600` file mode .
In Docker you can mount either the JSON file, or the folder containing it: In Docker you can mount either the JSON file, or the folder containing it:

View file

@ -37,6 +37,10 @@ spec:
port: 8080 port: 8080
tls: tls:
certResolver: myresolver certResolver: myresolver
domains:
- main: company.org
sans:
- *.company.org
``` ```
```json tab="Marathon" ```json tab="Marathon"

View file

@ -491,6 +491,30 @@ providers:
Defines the polling interval (in seconds) in Swarm Mode. Defines the polling interval (in seconds) in Swarm Mode.
### `watch`
_Optional, Default=true_
```toml tab="File (TOML)"
[providers.docker]
watch = false
# ...
```
```yaml tab="File (YAML)"
providers:
docker:
watch: false
# ...
```
```bash tab="CLI"
--providers.docker.watch=false
# ...
```
Watch Docker Swarm events.
### `constraints` ### `constraints`
_Optional, Default=""_ _Optional, Default=""_

View file

@ -406,7 +406,7 @@ TLS key
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```) Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`--providers.docker.watch`: `--providers.docker.watch`:
Watch provider. (Default: ```true```) Watch Docker Swarm events. (Default: ```true```)
`--providers.etcd`: `--providers.etcd`:
Enable Etcd backend with default settings. (Default: ```false```) Enable Etcd backend with default settings. (Default: ```false```)

View file

@ -406,7 +406,7 @@ TLS key
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```) Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_WATCH`: `TRAEFIK_PROVIDERS_DOCKER_WATCH`:
Watch provider. (Default: ```true```) Watch Docker Swarm events. (Default: ```true```)
`TRAEFIK_PROVIDERS_ETCD`: `TRAEFIK_PROVIDERS_ETCD`:
Enable Etcd backend with default settings. (Default: ```false```) Enable Etcd backend with default settings. (Default: ```false```)

View file

@ -460,6 +460,11 @@ func (in *Headers) DeepCopyInto(out *Headers) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.AccessControlAllowOriginList != nil {
in, out := &in.AccessControlAllowOriginList, &out.AccessControlAllowOriginList
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.AccessControlExposeHeaders != nil { if in.AccessControlExposeHeaders != nil {
in, out := &in.AccessControlExposeHeaders, &out.AccessControlExposeHeaders in, out := &in.AccessControlExposeHeaders, &out.AccessControlExposeHeaders
*out = make([]string, len(*in)) *out = make([]string, len(*in))

View file

@ -50,14 +50,14 @@ func RegisterDatadog(ctx context.Context, config *types.Datadog) Registry {
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {
registry.epEnabled = config.AddEntryPointsLabels registry.epEnabled = config.AddEntryPointsLabels
registry.entryPointReqsCounter = datadogClient.NewCounter(ddEntryPointReqsName, 1.0) registry.entryPointReqsCounter = datadogClient.NewCounter(ddEntryPointReqsName, 1.0)
registry.entryPointReqDurationHistogram = datadogClient.NewHistogram(ddEntryPointReqDurationName, 1.0) registry.entryPointReqDurationHistogram, _ = NewHistogramWithScale(datadogClient.NewHistogram(ddEntryPointReqDurationName, 1.0), time.Second)
registry.entryPointOpenConnsGauge = datadogClient.NewGauge(ddEntryPointOpenConnsName) registry.entryPointOpenConnsGauge = datadogClient.NewGauge(ddEntryPointOpenConnsName)
} }
if config.AddServicesLabels { if config.AddServicesLabels {
registry.svcEnabled = config.AddServicesLabels registry.svcEnabled = config.AddServicesLabels
registry.serviceReqsCounter = datadogClient.NewCounter(ddMetricsServiceReqsName, 1.0) registry.serviceReqsCounter = datadogClient.NewCounter(ddMetricsServiceReqsName, 1.0)
registry.serviceReqDurationHistogram = datadogClient.NewHistogram(ddMetricsServiceLatencyName, 1.0) registry.serviceReqDurationHistogram, _ = NewHistogramWithScale(datadogClient.NewHistogram(ddMetricsServiceLatencyName, 1.0), time.Second)
registry.serviceRetriesCounter = datadogClient.NewCounter(ddRetriesTotalName, 1.0) registry.serviceRetriesCounter = datadogClient.NewCounter(ddRetriesTotalName, 1.0)
registry.serviceOpenConnsGauge = datadogClient.NewGauge(ddOpenConnsName) registry.serviceOpenConnsGauge = datadogClient.NewGauge(ddOpenConnsName)
registry.serviceServerUpGauge = datadogClient.NewGauge(ddServerUpName) registry.serviceServerUpGauge = datadogClient.NewGauge(ddServerUpName)

View file

@ -64,14 +64,14 @@ func RegisterInfluxDB(ctx context.Context, config *types.InfluxDB) Registry {
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {
registry.epEnabled = config.AddEntryPointsLabels registry.epEnabled = config.AddEntryPointsLabels
registry.entryPointReqsCounter = influxDBClient.NewCounter(influxDBEntryPointReqsName) registry.entryPointReqsCounter = influxDBClient.NewCounter(influxDBEntryPointReqsName)
registry.entryPointReqDurationHistogram = influxDBClient.NewHistogram(influxDBEntryPointReqDurationName) registry.entryPointReqDurationHistogram, _ = NewHistogramWithScale(influxDBClient.NewHistogram(influxDBEntryPointReqDurationName), time.Second)
registry.entryPointOpenConnsGauge = influxDBClient.NewGauge(influxDBEntryPointOpenConnsName) registry.entryPointOpenConnsGauge = influxDBClient.NewGauge(influxDBEntryPointOpenConnsName)
} }
if config.AddServicesLabels { if config.AddServicesLabels {
registry.svcEnabled = config.AddServicesLabels registry.svcEnabled = config.AddServicesLabels
registry.serviceReqsCounter = influxDBClient.NewCounter(influxDBMetricsServiceReqsName) registry.serviceReqsCounter = influxDBClient.NewCounter(influxDBMetricsServiceReqsName)
registry.serviceReqDurationHistogram = influxDBClient.NewHistogram(influxDBMetricsServiceLatencyName) registry.serviceReqDurationHistogram, _ = NewHistogramWithScale(influxDBClient.NewHistogram(influxDBMetricsServiceLatencyName), time.Second)
registry.serviceRetriesCounter = influxDBClient.NewCounter(influxDBRetriesTotalName) registry.serviceRetriesCounter = influxDBClient.NewCounter(influxDBRetriesTotalName)
registry.serviceOpenConnsGauge = influxDBClient.NewGauge(influxDBOpenConnsName) registry.serviceOpenConnsGauge = influxDBClient.NewGauge(influxDBOpenConnsName)
registry.serviceServerUpGauge = influxDBClient.NewGauge(influxDBServerUpName) registry.serviceServerUpGauge = influxDBClient.NewGauge(influxDBServerUpName)

View file

@ -1,6 +1,9 @@
package metrics package metrics
import ( import (
"errors"
"time"
"github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/multi" "github.com/go-kit/kit/metrics/multi"
) )
@ -21,13 +24,13 @@ type Registry interface {
// entry point metrics // entry point metrics
EntryPointReqsCounter() metrics.Counter EntryPointReqsCounter() metrics.Counter
EntryPointReqsTLSCounter() metrics.Counter EntryPointReqsTLSCounter() metrics.Counter
EntryPointReqDurationHistogram() metrics.Histogram EntryPointReqDurationHistogram() ScalableHistogram
EntryPointOpenConnsGauge() metrics.Gauge EntryPointOpenConnsGauge() metrics.Gauge
// service metrics // service metrics
ServiceReqsCounter() metrics.Counter ServiceReqsCounter() metrics.Counter
ServiceReqsTLSCounter() metrics.Counter ServiceReqsTLSCounter() metrics.Counter
ServiceReqDurationHistogram() metrics.Histogram ServiceReqDurationHistogram() ScalableHistogram
ServiceOpenConnsGauge() metrics.Gauge ServiceOpenConnsGauge() metrics.Gauge
ServiceRetriesCounter() metrics.Counter ServiceRetriesCounter() metrics.Counter
ServiceServerUpGauge() metrics.Gauge ServiceServerUpGauge() metrics.Gauge
@ -49,11 +52,11 @@ func NewMultiRegistry(registries []Registry) Registry {
var lastConfigReloadFailureGauge []metrics.Gauge var lastConfigReloadFailureGauge []metrics.Gauge
var entryPointReqsCounter []metrics.Counter var entryPointReqsCounter []metrics.Counter
var entryPointReqsTLSCounter []metrics.Counter var entryPointReqsTLSCounter []metrics.Counter
var entryPointReqDurationHistogram []metrics.Histogram var entryPointReqDurationHistogram []ScalableHistogram
var entryPointOpenConnsGauge []metrics.Gauge var entryPointOpenConnsGauge []metrics.Gauge
var serviceReqsCounter []metrics.Counter var serviceReqsCounter []metrics.Counter
var serviceReqsTLSCounter []metrics.Counter var serviceReqsTLSCounter []metrics.Counter
var serviceReqDurationHistogram []metrics.Histogram var serviceReqDurationHistogram []ScalableHistogram
var serviceOpenConnsGauge []metrics.Gauge var serviceOpenConnsGauge []metrics.Gauge
var serviceRetriesCounter []metrics.Counter var serviceRetriesCounter []metrics.Counter
var serviceServerUpGauge []metrics.Gauge var serviceServerUpGauge []metrics.Gauge
@ -112,11 +115,11 @@ func NewMultiRegistry(registries []Registry) Registry {
lastConfigReloadFailureGauge: multi.NewGauge(lastConfigReloadFailureGauge...), lastConfigReloadFailureGauge: multi.NewGauge(lastConfigReloadFailureGauge...),
entryPointReqsCounter: multi.NewCounter(entryPointReqsCounter...), entryPointReqsCounter: multi.NewCounter(entryPointReqsCounter...),
entryPointReqsTLSCounter: multi.NewCounter(entryPointReqsTLSCounter...), entryPointReqsTLSCounter: multi.NewCounter(entryPointReqsTLSCounter...),
entryPointReqDurationHistogram: multi.NewHistogram(entryPointReqDurationHistogram...), entryPointReqDurationHistogram: NewMultiHistogram(entryPointReqDurationHistogram...),
entryPointOpenConnsGauge: multi.NewGauge(entryPointOpenConnsGauge...), entryPointOpenConnsGauge: multi.NewGauge(entryPointOpenConnsGauge...),
serviceReqsCounter: multi.NewCounter(serviceReqsCounter...), serviceReqsCounter: multi.NewCounter(serviceReqsCounter...),
serviceReqsTLSCounter: multi.NewCounter(serviceReqsTLSCounter...), serviceReqsTLSCounter: multi.NewCounter(serviceReqsTLSCounter...),
serviceReqDurationHistogram: multi.NewHistogram(serviceReqDurationHistogram...), serviceReqDurationHistogram: NewMultiHistogram(serviceReqDurationHistogram...),
serviceOpenConnsGauge: multi.NewGauge(serviceOpenConnsGauge...), serviceOpenConnsGauge: multi.NewGauge(serviceOpenConnsGauge...),
serviceRetriesCounter: multi.NewCounter(serviceRetriesCounter...), serviceRetriesCounter: multi.NewCounter(serviceRetriesCounter...),
serviceServerUpGauge: multi.NewGauge(serviceServerUpGauge...), serviceServerUpGauge: multi.NewGauge(serviceServerUpGauge...),
@ -132,11 +135,11 @@ type standardRegistry struct {
lastConfigReloadFailureGauge metrics.Gauge lastConfigReloadFailureGauge metrics.Gauge
entryPointReqsCounter metrics.Counter entryPointReqsCounter metrics.Counter
entryPointReqsTLSCounter metrics.Counter entryPointReqsTLSCounter metrics.Counter
entryPointReqDurationHistogram metrics.Histogram entryPointReqDurationHistogram ScalableHistogram
entryPointOpenConnsGauge metrics.Gauge entryPointOpenConnsGauge metrics.Gauge
serviceReqsCounter metrics.Counter serviceReqsCounter metrics.Counter
serviceReqsTLSCounter metrics.Counter serviceReqsTLSCounter metrics.Counter
serviceReqDurationHistogram metrics.Histogram serviceReqDurationHistogram ScalableHistogram
serviceOpenConnsGauge metrics.Gauge serviceOpenConnsGauge metrics.Gauge
serviceRetriesCounter metrics.Counter serviceRetriesCounter metrics.Counter
serviceServerUpGauge metrics.Gauge serviceServerUpGauge metrics.Gauge
@ -174,7 +177,7 @@ func (r *standardRegistry) EntryPointReqsTLSCounter() metrics.Counter {
return r.entryPointReqsTLSCounter return r.entryPointReqsTLSCounter
} }
func (r *standardRegistry) EntryPointReqDurationHistogram() metrics.Histogram { func (r *standardRegistry) EntryPointReqDurationHistogram() ScalableHistogram {
return r.entryPointReqDurationHistogram return r.entryPointReqDurationHistogram
} }
@ -190,7 +193,7 @@ func (r *standardRegistry) ServiceReqsTLSCounter() metrics.Counter {
return r.serviceReqsTLSCounter return r.serviceReqsTLSCounter
} }
func (r *standardRegistry) ServiceReqDurationHistogram() metrics.Histogram { func (r *standardRegistry) ServiceReqDurationHistogram() ScalableHistogram {
return r.serviceReqDurationHistogram return r.serviceReqDurationHistogram
} }
@ -205,3 +208,97 @@ func (r *standardRegistry) ServiceRetriesCounter() metrics.Counter {
func (r *standardRegistry) ServiceServerUpGauge() metrics.Gauge { func (r *standardRegistry) ServiceServerUpGauge() metrics.Gauge {
return r.serviceServerUpGauge return r.serviceServerUpGauge
} }
// ScalableHistogram is a Histogram with a predefined time unit,
// used when producing observations without explicitly setting the observed value.
type ScalableHistogram interface {
With(labelValues ...string) ScalableHistogram
StartAt(t time.Time)
Observe(v float64)
ObserveDuration()
}
// HistogramWithScale is a histogram that will convert its observed value to the specified unit.
type HistogramWithScale struct {
histogram metrics.Histogram
unit time.Duration
start time.Time
}
// With implements ScalableHistogram.
func (s *HistogramWithScale) With(labelValues ...string) ScalableHistogram {
s.histogram = s.histogram.With(labelValues...)
return s
}
// StartAt implements ScalableHistogram.
func (s *HistogramWithScale) StartAt(t time.Time) {
s.start = t
}
// ObserveDuration implements ScalableHistogram.
func (s *HistogramWithScale) ObserveDuration() {
if s.unit <= 0 {
return
}
d := float64(time.Since(s.start).Nanoseconds()) / float64(s.unit)
if d < 0 {
d = 0
}
s.histogram.Observe(d)
}
// Observe implements ScalableHistogram.
func (s *HistogramWithScale) Observe(v float64) {
s.histogram.Observe(v)
}
// NewHistogramWithScale returns a ScalableHistogram. It returns an error if the given unit is <= 0.
func NewHistogramWithScale(histogram metrics.Histogram, unit time.Duration) (ScalableHistogram, error) {
if unit <= 0 {
return nil, errors.New("invalid time unit")
}
return &HistogramWithScale{
histogram: histogram,
unit: unit,
}, nil
}
// MultiHistogram collects multiple individual histograms and treats them as a unit.
type MultiHistogram []ScalableHistogram
// NewMultiHistogram returns a multi-histogram, wrapping the passed histograms.
func NewMultiHistogram(h ...ScalableHistogram) MultiHistogram {
return MultiHistogram(h)
}
// StartAt implements ScalableHistogram.
func (h MultiHistogram) StartAt(t time.Time) {
for _, histogram := range h {
histogram.StartAt(t)
}
}
// ObserveDuration implements ScalableHistogram.
func (h MultiHistogram) ObserveDuration() {
for _, histogram := range h {
histogram.ObserveDuration()
}
}
// Observe implements ScalableHistogram.
func (h MultiHistogram) Observe(v float64) {
for _, histogram := range h {
histogram.Observe(v)
}
}
// With implements ScalableHistogram.
func (h MultiHistogram) With(labelValues ...string) ScalableHistogram {
next := make(MultiHistogram, len(h))
for i := range h {
next[i] = h[i].With(labelValues...)
}
return next
}

View file

@ -1,18 +1,44 @@
package metrics package metrics
import ( import (
"bytes"
"strings"
"testing" "testing"
"time"
"github.com/go-kit/kit/metrics" "github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestScalableHistogram(t *testing.T) {
h := generic.NewHistogram("test", 1)
sh, err := NewHistogramWithScale(h, time.Millisecond)
require.NoError(t, err)
ticker := time.NewTicker(500 * time.Millisecond)
<-ticker.C
sh.StartAt(time.Now())
<-ticker.C
sh.ObserveDuration()
var b bytes.Buffer
h.Print(&b)
extractedDurationString := strings.Split(strings.Split(b.String(), "\n")[1], " ")
measuredDuration, err := time.ParseDuration(extractedDurationString[0] + "ms")
assert.NoError(t, err)
assert.InDelta(t, 500*time.Millisecond, measuredDuration, float64(1*time.Millisecond))
}
func TestNewMultiRegistry(t *testing.T) { func TestNewMultiRegistry(t *testing.T) {
registries := []Registry{newCollectingRetryMetrics(), newCollectingRetryMetrics()} registries := []Registry{newCollectingRetryMetrics(), newCollectingRetryMetrics()}
registry := NewMultiRegistry(registries) registry := NewMultiRegistry(registries)
registry.ServiceReqsCounter().With("key", "requests").Add(1) registry.ServiceReqsCounter().With("key", "requests").Add(1)
registry.ServiceReqDurationHistogram().With("key", "durations").Observe(2) registry.ServiceReqDurationHistogram().With("key", "durations").Observe(float64(2))
registry.ServiceRetriesCounter().With("key", "retries").Add(3) registry.ServiceRetriesCounter().With("key", "retries").Add(3)
for _, collectingRegistry := range registries { for _, collectingRegistry := range registries {
@ -66,11 +92,17 @@ type histogramMock struct {
lastLabelValues []string lastLabelValues []string
} }
func (c *histogramMock) With(labelValues ...string) metrics.Histogram { func (c *histogramMock) With(labelValues ...string) ScalableHistogram {
c.lastLabelValues = labelValues c.lastLabelValues = labelValues
return c return c
} }
func (c *histogramMock) Observe(value float64) { func (c *histogramMock) Start() {}
c.lastHistogramValue = value
func (c *histogramMock) StartAt(t time.Time) {}
func (c *histogramMock) ObserveDuration() {}
func (c *histogramMock) Observe(v float64) {
c.lastHistogramValue = v
} }

View file

@ -6,6 +6,7 @@ import (
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
@ -160,7 +161,7 @@ func initStandardRegistry(config *types.Prometheus) Registry {
}...) }...)
reg.entryPointReqsCounter = entryPointReqs reg.entryPointReqsCounter = entryPointReqs
reg.entryPointReqsTLSCounter = entryPointReqsTLS reg.entryPointReqsTLSCounter = entryPointReqsTLS
reg.entryPointReqDurationHistogram = entryPointReqDurations reg.entryPointReqDurationHistogram, _ = NewHistogramWithScale(entryPointReqDurations, time.Second)
reg.entryPointOpenConnsGauge = entryPointOpenConns reg.entryPointOpenConnsGauge = entryPointOpenConns
} }
if config.AddServicesLabels { if config.AddServicesLabels {
@ -201,7 +202,7 @@ func initStandardRegistry(config *types.Prometheus) Registry {
reg.serviceReqsCounter = serviceReqs reg.serviceReqsCounter = serviceReqs
reg.serviceReqsTLSCounter = serviceReqsTLS reg.serviceReqsTLSCounter = serviceReqsTLS
reg.serviceReqDurationHistogram = serviceReqDurations reg.serviceReqDurationHistogram, _ = NewHistogramWithScale(serviceReqDurations, time.Second)
reg.serviceOpenConnsGauge = serviceOpenConns reg.serviceOpenConnsGauge = serviceOpenConns
reg.serviceRetriesCounter = serviceRetries reg.serviceRetriesCounter = serviceRetries
reg.serviceServerUpGauge = serviceServerUp reg.serviceServerUpGauge = serviceServerUp

View file

@ -55,14 +55,14 @@ func RegisterStatsd(ctx context.Context, config *types.Statsd) Registry {
if config.AddEntryPointsLabels { if config.AddEntryPointsLabels {
registry.epEnabled = config.AddEntryPointsLabels registry.epEnabled = config.AddEntryPointsLabels
registry.entryPointReqsCounter = statsdClient.NewCounter(statsdEntryPointReqsName, 1.0) registry.entryPointReqsCounter = statsdClient.NewCounter(statsdEntryPointReqsName, 1.0)
registry.entryPointReqDurationHistogram = statsdClient.NewTiming(statsdEntryPointReqDurationName, 1.0) registry.entryPointReqDurationHistogram, _ = NewHistogramWithScale(statsdClient.NewTiming(statsdEntryPointReqDurationName, 1.0), time.Millisecond)
registry.entryPointOpenConnsGauge = statsdClient.NewGauge(statsdEntryPointOpenConnsName) registry.entryPointOpenConnsGauge = statsdClient.NewGauge(statsdEntryPointOpenConnsName)
} }
if config.AddServicesLabels { if config.AddServicesLabels {
registry.svcEnabled = config.AddServicesLabels registry.svcEnabled = config.AddServicesLabels
registry.serviceReqsCounter = statsdClient.NewCounter(statsdMetricsServiceReqsName, 1.0) registry.serviceReqsCounter = statsdClient.NewCounter(statsdMetricsServiceReqsName, 1.0)
registry.serviceReqDurationHistogram = statsdClient.NewTiming(statsdMetricsServiceLatencyName, 1.0) registry.serviceReqDurationHistogram, _ = NewHistogramWithScale(statsdClient.NewTiming(statsdMetricsServiceLatencyName, 1.0), time.Millisecond)
registry.serviceRetriesCounter = statsdClient.NewCounter(statsdRetriesTotalName, 1.0) registry.serviceRetriesCounter = statsdClient.NewCounter(statsdRetriesTotalName, 1.0)
registry.serviceOpenConnsGauge = statsdClient.NewGauge(statsdOpenConnsName) registry.serviceOpenConnsGauge = statsdClient.NewGauge(statsdOpenConnsName)
registry.serviceServerUpGauge = statsdClient.NewGauge(statsdServerUpName) registry.serviceServerUpGauge = statsdClient.NewGauge(statsdServerUpName)

View file

@ -31,7 +31,7 @@ type metricsMiddleware struct {
next http.Handler next http.Handler
reqsCounter gokitmetrics.Counter reqsCounter gokitmetrics.Counter
reqsTLSCounter gokitmetrics.Counter reqsTLSCounter gokitmetrics.Counter
reqDurationHistogram gokitmetrics.Histogram reqDurationHistogram metrics.ScalableHistogram
openConnsGauge gokitmetrics.Gauge openConnsGauge gokitmetrics.Gauge
baseLabels []string baseLabels []string
} }
@ -97,13 +97,17 @@ func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request)
recorder := newResponseRecorder(rw) recorder := newResponseRecorder(rw)
start := time.Now() start := time.Now()
m.next.ServeHTTP(recorder, req) m.next.ServeHTTP(recorder, req)
duration := time.Since(start).Seconds()
labels = append(labels, "code", strconv.Itoa(recorder.getCode())) labels = append(labels, "code", strconv.Itoa(recorder.getCode()))
histograms := m.reqDurationHistogram.With(labels...)
histograms.StartAt(start)
m.reqsCounter.With(labels...).Add(1) m.reqsCounter.With(labels...).Add(1)
m.reqDurationHistogram.With(labels...).Observe(duration)
histograms.ObserveDuration()
} }
func getRequestProtocol(req *http.Request) string { func getRequestProtocol(req *http.Request) string {

View file

@ -3,6 +3,7 @@ package replacepath
import ( import (
"context" "context"
"net/http" "net/http"
"net/url"
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
@ -40,8 +41,22 @@ func (r *replacePath) GetTracingInformation() (string, ext.SpanKindEnum) {
} }
func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
req.Header.Add(ReplacedPathHeader, req.URL.Path) if req.URL.RawPath == "" {
req.URL.Path = r.path req.Header.Add(ReplacedPathHeader, req.URL.Path)
} else {
req.Header.Add(ReplacedPathHeader, req.URL.RawPath)
}
req.URL.RawPath = r.path
var err error
req.URL.Path, err = url.PathUnescape(req.URL.RawPath)
if err != nil {
log.FromContext(middlewares.GetLoggerCtx(context.Background(), r.name, typeName)).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
req.RequestURI = req.URL.RequestURI() req.RequestURI = req.URL.RequestURI()
r.next.ServeHTTP(rw, req) r.next.ServeHTTP(rw, req)

View file

@ -3,43 +3,93 @@ package replacepath
import ( import (
"context" "context"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestReplacePath(t *testing.T) { func TestReplacePath(t *testing.T) {
var replacementConfig = dynamic.ReplacePath{ testCases := []struct {
Path: "/replacement-path", desc string
path string
config dynamic.ReplacePath
expectedPath string
expectedRawPath string
expectedHeader string
}{
{
desc: "simple path",
path: "/example",
config: dynamic.ReplacePath{
Path: "/replacement-path",
},
expectedPath: "/replacement-path",
expectedRawPath: "",
expectedHeader: "/example",
},
{
desc: "long path",
path: "/some/really/long/path",
config: dynamic.ReplacePath{
Path: "/replacement-path",
},
expectedPath: "/replacement-path",
expectedRawPath: "",
expectedHeader: "/some/really/long/path",
},
{
desc: "path with escaped value",
path: "/foo%2Fbar",
config: dynamic.ReplacePath{
Path: "/replacement-path",
},
expectedPath: "/replacement-path",
expectedRawPath: "",
expectedHeader: "/foo%2Fbar",
},
{
desc: "replacement with escaped value",
path: "/path",
config: dynamic.ReplacePath{
Path: "/foo%2Fbar",
},
expectedPath: "/foo/bar",
expectedRawPath: "/foo%2Fbar",
expectedHeader: "/path",
},
} }
paths := []string{ for _, test := range testCases {
"/example", t.Run(test.desc, func(t *testing.T) {
"/some/really/long/path", var actualPath, actualRawPath, actualHeader, requestURI string
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
var expectedPath, actualHeader, requestURI string
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expectedPath = r.URL.Path actualPath = r.URL.Path
actualRawPath = r.URL.RawPath
actualHeader = r.Header.Get(ReplacedPathHeader) actualHeader = r.Header.Get(ReplacedPathHeader)
requestURI = r.RequestURI requestURI = r.RequestURI
}) })
handler, err := New(context.Background(), next, replacementConfig, "foo-replace-path") handler, err := New(context.Background(), next, test.config, "foo-replace-path")
require.NoError(t, err) require.NoError(t, err)
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+path, nil) server := httptest.NewServer(handler)
defer server.Close()
handler.ServeHTTP(nil, req) resp, err := http.Get(server.URL + test.path)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, expectedPath, replacementConfig.Path, "Unexpected path.") assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
assert.Equal(t, path, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader) assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader)
assert.Equal(t, expectedPath, requestURI, "Unexpected request URI.")
if actualRawPath == "" {
assert.Equal(t, actualPath, requestURI, "Unexpected request URI.")
} else {
assert.Equal(t, actualRawPath, requestURI, "Unexpected request URI.")
}
}) })
} }
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"strings" "strings"
@ -49,10 +50,31 @@ func (rp *replacePathRegex) GetTracingInformation() (string, ext.SpanKindEnum) {
} }
func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if rp.regexp != nil && len(rp.replacement) > 0 && rp.regexp.MatchString(req.URL.Path) { var currentPath string
req.Header.Add(replacepath.ReplacedPathHeader, req.URL.Path) if req.URL.RawPath == "" {
req.URL.Path = rp.regexp.ReplaceAllString(req.URL.Path, rp.replacement) currentPath = req.URL.Path
} else {
currentPath = req.URL.RawPath
}
if rp.regexp != nil && len(rp.replacement) > 0 && rp.regexp.MatchString(currentPath) {
req.Header.Add(replacepath.ReplacedPathHeader, currentPath)
req.URL.RawPath = rp.regexp.ReplaceAllString(currentPath, rp.replacement)
// as replacement can introduce escaped characters
// Path must remain an unescaped version of RawPath
// Doesn't handle multiple times encoded replacement (`/` => `%2F` => `%252F` => ...)
var err error
req.URL.Path, err = url.PathUnescape(req.URL.RawPath)
if err != nil {
log.FromContext(middlewares.GetLoggerCtx(context.Background(), rp.name, typeName)).Error(err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
req.RequestURI = req.URL.RequestURI() req.RequestURI = req.URL.RequestURI()
} }
rp.next.ServeHTTP(rw, req) rp.next.ServeHTTP(rw, req)
} }

View file

@ -3,23 +3,24 @@ package replacepathregex
import ( import (
"context" "context"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/middlewares/replacepath" "github.com/containous/traefik/v2/pkg/middlewares/replacepath"
"github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestReplacePathRegex(t *testing.T) { func TestReplacePathRegex(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
path string path string
config dynamic.ReplacePathRegex config dynamic.ReplacePathRegex
expectedPath string expectedPath string
expectedHeader string expectedRawPath string
expectsError bool expectedHeader string
expectsError bool
}{ }{
{ {
desc: "simple regex", desc: "simple regex",
@ -28,8 +29,9 @@ func TestReplacePathRegex(t *testing.T) {
Replacement: "/who-am-i/$1", Replacement: "/who-am-i/$1",
Regex: `^/whoami/(.*)`, Regex: `^/whoami/(.*)`,
}, },
expectedPath: "/who-am-i/and/whoami", expectedPath: "/who-am-i/and/whoami",
expectedHeader: "/whoami/and/whoami", expectedRawPath: "/who-am-i/and/whoami",
expectedHeader: "/whoami/and/whoami",
}, },
{ {
desc: "simple replace (no regex)", desc: "simple replace (no regex)",
@ -38,8 +40,9 @@ func TestReplacePathRegex(t *testing.T) {
Replacement: "/who-am-i", Replacement: "/who-am-i",
Regex: `/whoami`, Regex: `/whoami`,
}, },
expectedPath: "/who-am-i/and/who-am-i", expectedPath: "/who-am-i/and/who-am-i",
expectedHeader: "/whoami/and/whoami", expectedRawPath: "/who-am-i/and/who-am-i",
expectedHeader: "/whoami/and/whoami",
}, },
{ {
desc: "no match", desc: "no match",
@ -57,8 +60,9 @@ func TestReplacePathRegex(t *testing.T) {
Replacement: "/downloads/$1-$2", Replacement: "/downloads/$1-$2",
Regex: `^(?i)/downloads/([^/]+)/([^/]+)$`, Regex: `^(?i)/downloads/([^/]+)/([^/]+)$`,
}, },
expectedPath: "/downloads/src-source.go", expectedPath: "/downloads/src-source.go",
expectedHeader: "/downloads/src/source.go", expectedRawPath: "/downloads/src-source.go",
expectedHeader: "/downloads/src/source.go",
}, },
{ {
desc: "invalid regular expression", desc: "invalid regular expression",
@ -70,13 +74,46 @@ func TestReplacePathRegex(t *testing.T) {
expectedPath: "/invalid/regexp/test", expectedPath: "/invalid/regexp/test",
expectsError: true, expectsError: true,
}, },
{
desc: "replacement with escaped char",
path: "/aaa/bbb",
config: dynamic.ReplacePathRegex{
Replacement: "/foo%2Fbar",
Regex: `/aaa/bbb`,
},
expectedPath: "/foo/bar",
expectedRawPath: "/foo%2Fbar",
expectedHeader: "/aaa/bbb",
},
{
desc: "path and regex with escaped char",
path: "/aaa%2Fbbb",
config: dynamic.ReplacePathRegex{
Replacement: "/foo/bar",
Regex: `/aaa%2Fbbb`,
},
expectedPath: "/foo/bar",
expectedRawPath: "/foo/bar",
expectedHeader: "/aaa%2Fbbb",
},
{
desc: "path with escaped char (no match)",
path: "/aaa%2Fbbb",
config: dynamic.ReplacePathRegex{
Replacement: "/foo/bar",
Regex: `/aaa/bbb`,
},
expectedPath: "/aaa/bbb",
expectedRawPath: "/aaa%2Fbbb",
},
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
var actualPath, actualHeader, requestURI string var actualPath, actualRawPath, actualHeader, requestURI string
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
actualPath = r.URL.Path actualPath = r.URL.Path
actualRawPath = r.URL.RawPath
actualHeader = r.Header.Get(replacepath.ReplacedPathHeader) actualHeader = r.Header.Get(replacepath.ReplacedPathHeader)
requestURI = r.RequestURI requestURI = r.RequestURI
}) })
@ -84,19 +121,29 @@ func TestReplacePathRegex(t *testing.T) {
handler, err := New(context.Background(), next, test.config, "foo-replace-path-regexp") handler, err := New(context.Background(), next, test.config, "foo-replace-path-regexp")
if test.expectsError { if test.expectsError {
require.Error(t, err) require.Error(t, err)
} else { return
require.NoError(t, err) }
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+test.path, nil) require.NoError(t, err)
req.RequestURI = test.path
handler.ServeHTTP(nil, req) server := httptest.NewServer(handler)
defer server.Close()
assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.") resp, err := http.Get(server.URL + test.path)
require.NoError(t, err, "Unexpected error while making test request")
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
if actualRawPath == "" {
assert.Equal(t, actualPath, requestURI, "Unexpected request URI.") assert.Equal(t, actualPath, requestURI, "Unexpected request URI.")
if test.expectedHeader != "" { } else {
assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", replacepath.ReplacedPathHeader) assert.Equal(t, actualRawPath, requestURI, "Unexpected request URI.")
} }
if test.expectedHeader != "" {
assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", replacepath.ReplacedPathHeader)
} }
}) })
} }

View file

@ -47,7 +47,7 @@ var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider. // Provider holds configurations of the provider.
type Provider struct { type Provider struct {
Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` Constraints string `description:"Constraints is an expression that Traefik matches against the container's labels to determine whether to create any route for that container." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"`
Watch bool `description:"Watch provider." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"` Watch bool `description:"Watch Docker Swarm events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"` Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"`
TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"` TLS *types.ClientTLS `description:"Enable Docker TLS support." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" export:"true"`