1
0
mirror of https://github.com/containous/traefik.git synced 2025-09-19 01:44:23 +03:00

Compare commits

...

9 Commits

Author SHA1 Message Date
Fernandez Ludovic
683d5d5a48 chore: skip openbsd/freebsd arm64 2020-03-18 17:21:20 +01:00
Ludovic Fernandez
4f92ef5fa9 Prepare release v2.1.7 2020-03-18 15:50:05 +01:00
Ludovic Fernandez
62c3025a76 Access log field quotes. 2020-03-17 12:36:04 +01:00
Traefiker Bot
b5d205b78c fix statsd scale for duration based metrics 2020-03-05 15:10:07 +01:00
Ludovic Fernandez
dccc075f2c Add some missing doc. 2020-03-04 16:48:05 +01:00
Ole Rößner
5fdec48854 Added wildcard ACME example 2020-03-04 13:24:05 +01:00
robotte
353bd3d06f Added support for replacement containing escaped characters
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2020-03-03 16:20:05 +01:00
Hamilton Turner
a7495f711b fix typo 2020-02-29 18:48:04 +01:00
Ludovic Fernandez
5072735866 Prepare release v2.1.6 2020-02-28 18:30:05 +01:00
23 changed files with 417 additions and 96 deletions

View File

@@ -34,8 +34,10 @@ builds:
goarch: 386
- goos: openbsd
goarch: arm
- goos: openbsd
goarch: arm64
- goos: freebsd
goarch: arm
goarch: arm64
changelog:
skip: true

View File

@@ -1,5 +1,18 @@
## [v2.1.5](https://github.com/containous/traefik/tree/v2.1.5) (2020-02-28)
[All Commits](https://github.com/containous/traefik/compare/v2.1.4...v2.1.5)
## [v2.1.7](https://github.com/containous/traefik/tree/v2.1.7) (2020-03-18)
[All Commits](https://github.com/containous/traefik/compare/v2.1.6...v2.1.7)
**Bug fixes:**
- **[logs,middleware]** Access log field quotes. ([#6484](https://github.com/containous/traefik/pull/6484) by [ldez](https://github.com/ldez))
- **[metrics]** fix statsd scale for duration based metrics ([#6054](https://github.com/containous/traefik/pull/6054) by [ddtmachado](https://github.com/ddtmachado))
- **[middleware]** Added support for replacement containing escaped characters ([#6413](https://github.com/containous/traefik/pull/6413) by [rtribotte](https://github.com/rtribotte))
**Documentation:**
- **[acme,docker]** Add some missing doc. ([#6422](https://github.com/containous/traefik/pull/6422) by [ldez](https://github.com/ldez))
- **[acme]** Added wildcard ACME example ([#6423](https://github.com/containous/traefik/pull/6423) by [Basster](https://github.com/Basster))
- **[acme]** fix typo ([#6408](https://github.com/containous/traefik/pull/6408) by [hamiltont](https://github.com/hamiltont))
## [v2.1.6](https://github.com/containous/traefik/tree/v2.1.6) (2020-02-28)
[All Commits](https://github.com/containous/traefik/compare/v2.1.4...v2.1.6)
**Bug fixes:**
- **[acme]** Update go-acme/lego to v3.4.0 ([#6376](https://github.com/containous/traefik/pull/6376) by [ldez](https://github.com/ldez))
@@ -28,6 +41,10 @@
- Minor readme improvements ([#6293](https://github.com/containous/traefik/pull/6293) by [Rowayda-Khayri](https://github.com/Rowayda-Khayri))
- Added link to community forum ([#6283](https://github.com/containous/traefik/pull/6283) by [isaacnewtonfx](https://github.com/isaacnewtonfx))
## [v2.1.5](https://github.com/containous/traefik/tree/v2.1.5) (2020-02-28)
Skipped.
## [v2.1.4](https://github.com/containous/traefik/tree/v2.1.4) (2020-02-06)
[All Commits](https://github.com/containous/traefik/compare/v2.1.3...v2.1.4)

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),
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)"
[entryPoints]
@@ -396,6 +396,13 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
### `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"
```toml tab="File (TOML)"
@@ -422,6 +429,8 @@ As described in [Let's Encrypt's post](https://community.letsencrypt.org/t/stagi
### `storage`
_Required, Default="acme.json"_
The `storage` option sets the location where your ACME certificates are saved to.
```toml tab="File (TOML)"
@@ -446,13 +455,7 @@ certificatesResolvers:
# ...
```
The value can refer to some kinds of storage:
- a JSON file
#### In a File
ACME certificates can be stored in a JSON file that needs to have a `600` file mode .
ACME certificates are 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:

View File

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

View File

@@ -35,7 +35,7 @@ If the given format is unsupported, the default (CLF) is used instead.
!!! info "Common Log Format"
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_router_name>" "<Traefik_server_URL>" <request_duration_in_ms>ms
```
### `bufferingSize`
@@ -195,6 +195,7 @@ accessLog:
| `RequestMethod` | The HTTP method. |
| `RequestPath` | The HTTP request URI, not including the scheme, host or port. |
| `RequestProtocol` | The version of HTTP requested. |
| `RequestScheme` | The HTTP scheme requested `http` or `https`. |
| `RequestLine` | `RequestMethod` + `RequestPath` + `RequestProtocol` |
| `RequestContentSize` | The number of bytes in the request entity (a.k.a. body) sent by the client. |
| `OriginDuration` | The time taken by the origin server ('upstream') to return its response. |

View File

@@ -452,6 +452,30 @@ providers:
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`
_Optional, Default=""_

View File

@@ -346,7 +346,7 @@ TLS key
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`--providers.docker.watch`:
Watch provider. (Default: ```true```)
Watch Docker Swarm events. (Default: ```true```)
`--providers.file.debugloggeneratedtemplate`:
Enable debug logging of generated configuration template. (Default: ```false```)
@@ -580,7 +580,7 @@ Specifies the header name prefix that will be used to store baggage items in a m
Key:Value tag to be set on all the spans.
`--tracing.haystack.localagenthost`:
Set haystack-agent's host that the reporter will used. (Default: ```LocalAgentHost```)
Set haystack-agent's host that the reporter will used. (Default: ```127.0.0.1```)
`--tracing.haystack.localagentport`:
Set haystack-agent's port that the reporter will used. (Default: ```35000```)

View File

@@ -346,7 +346,7 @@ TLS key
Use the ip address from the bound port, rather than from the inner network. (Default: ```false```)
`TRAEFIK_PROVIDERS_DOCKER_WATCH`:
Watch provider. (Default: ```true```)
Watch Docker Swarm events. (Default: ```true```)
`TRAEFIK_PROVIDERS_FILE_DEBUGLOGGENERATEDTEMPLATE`:
Enable debug logging of generated configuration template. (Default: ```false```)
@@ -580,7 +580,7 @@ Specifies the header name prefix that will be used to store baggage items in a m
Key:Value tag to be set on all the spans.
`TRAEFIK_TRACING_HAYSTACK_LOCALAGENTHOST`:
Set haystack-agent's host that the reporter will used. (Default: ```LocalAgentHost```)
Set haystack-agent's host that the reporter will used. (Default: ```127.0.0.1```)
`TRAEFIK_TRACING_HAYSTACK_LOCALAGENTPORT`:
Set haystack-agent's port that the reporter will used. (Default: ```35000```)

View File

@@ -50,14 +50,14 @@ func RegisterDatadog(ctx context.Context, config *types.Datadog) Registry {
if config.AddEntryPointsLabels {
registry.epEnabled = config.AddEntryPointsLabels
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)
}
if config.AddServicesLabels {
registry.svcEnabled = config.AddServicesLabels
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.serviceOpenConnsGauge = datadogClient.NewGauge(ddOpenConnsName)
registry.serviceServerUpGauge = datadogClient.NewGauge(ddServerUpName)

View File

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

View File

@@ -1,6 +1,9 @@
package metrics
import (
"errors"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/multi"
)
@@ -20,12 +23,12 @@ type Registry interface {
// entry point metrics
EntryPointReqsCounter() metrics.Counter
EntryPointReqDurationHistogram() metrics.Histogram
EntryPointReqDurationHistogram() ScalableHistogram
EntryPointOpenConnsGauge() metrics.Gauge
// service metrics
ServiceReqsCounter() metrics.Counter
ServiceReqDurationHistogram() metrics.Histogram
ServiceReqDurationHistogram() ScalableHistogram
ServiceOpenConnsGauge() metrics.Gauge
ServiceRetriesCounter() metrics.Counter
ServiceServerUpGauge() metrics.Gauge
@@ -46,10 +49,10 @@ func NewMultiRegistry(registries []Registry) Registry {
var lastConfigReloadSuccessGauge []metrics.Gauge
var lastConfigReloadFailureGauge []metrics.Gauge
var entryPointReqsCounter []metrics.Counter
var entryPointReqDurationHistogram []metrics.Histogram
var entryPointReqDurationHistogram []ScalableHistogram
var entryPointOpenConnsGauge []metrics.Gauge
var serviceReqsCounter []metrics.Counter
var serviceReqDurationHistogram []metrics.Histogram
var serviceReqDurationHistogram []ScalableHistogram
var serviceOpenConnsGauge []metrics.Gauge
var serviceRetriesCounter []metrics.Counter
var serviceServerUpGauge []metrics.Gauge
@@ -101,10 +104,10 @@ func NewMultiRegistry(registries []Registry) Registry {
lastConfigReloadSuccessGauge: multi.NewGauge(lastConfigReloadSuccessGauge...),
lastConfigReloadFailureGauge: multi.NewGauge(lastConfigReloadFailureGauge...),
entryPointReqsCounter: multi.NewCounter(entryPointReqsCounter...),
entryPointReqDurationHistogram: multi.NewHistogram(entryPointReqDurationHistogram...),
entryPointReqDurationHistogram: NewMultiHistogram(entryPointReqDurationHistogram...),
entryPointOpenConnsGauge: multi.NewGauge(entryPointOpenConnsGauge...),
serviceReqsCounter: multi.NewCounter(serviceReqsCounter...),
serviceReqDurationHistogram: multi.NewHistogram(serviceReqDurationHistogram...),
serviceReqDurationHistogram: NewMultiHistogram(serviceReqDurationHistogram...),
serviceOpenConnsGauge: multi.NewGauge(serviceOpenConnsGauge...),
serviceRetriesCounter: multi.NewCounter(serviceRetriesCounter...),
serviceServerUpGauge: multi.NewGauge(serviceServerUpGauge...),
@@ -119,10 +122,10 @@ type standardRegistry struct {
lastConfigReloadSuccessGauge metrics.Gauge
lastConfigReloadFailureGauge metrics.Gauge
entryPointReqsCounter metrics.Counter
entryPointReqDurationHistogram metrics.Histogram
entryPointReqDurationHistogram ScalableHistogram
entryPointOpenConnsGauge metrics.Gauge
serviceReqsCounter metrics.Counter
serviceReqDurationHistogram metrics.Histogram
serviceReqDurationHistogram ScalableHistogram
serviceOpenConnsGauge metrics.Gauge
serviceRetriesCounter metrics.Counter
serviceServerUpGauge metrics.Gauge
@@ -156,7 +159,7 @@ func (r *standardRegistry) EntryPointReqsCounter() metrics.Counter {
return r.entryPointReqsCounter
}
func (r *standardRegistry) EntryPointReqDurationHistogram() metrics.Histogram {
func (r *standardRegistry) EntryPointReqDurationHistogram() ScalableHistogram {
return r.entryPointReqDurationHistogram
}
@@ -168,7 +171,7 @@ func (r *standardRegistry) ServiceReqsCounter() metrics.Counter {
return r.serviceReqsCounter
}
func (r *standardRegistry) ServiceReqDurationHistogram() metrics.Histogram {
func (r *standardRegistry) ServiceReqDurationHistogram() ScalableHistogram {
return r.serviceReqDurationHistogram
}
@@ -183,3 +186,97 @@ func (r *standardRegistry) ServiceRetriesCounter() metrics.Counter {
func (r *standardRegistry) ServiceServerUpGauge() metrics.Gauge {
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
import (
"bytes"
"strings"
"testing"
"time"
"github.com/go-kit/kit/metrics"
"github.com/go-kit/kit/metrics/generic"
"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) {
registries := []Registry{newCollectingRetryMetrics(), newCollectingRetryMetrics()}
registry := NewMultiRegistry(registries)
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)
for _, collectingRegistry := range registries {
@@ -66,11 +92,17 @@ type histogramMock struct {
lastLabelValues []string
}
func (c *histogramMock) With(labelValues ...string) metrics.Histogram {
func (c *histogramMock) With(labelValues ...string) ScalableHistogram {
c.lastLabelValues = labelValues
return c
}
func (c *histogramMock) Observe(value float64) {
c.lastHistogramValue = value
func (c *histogramMock) Start() {}
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"
"strings"
"sync"
"time"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log"
@@ -152,7 +153,7 @@ func initStandardRegistry(config *types.Prometheus) Registry {
entryPointOpenConns.gv.Describe,
}...)
reg.entryPointReqsCounter = entryPointReqs
reg.entryPointReqDurationHistogram = entryPointReqDurations
reg.entryPointReqDurationHistogram, _ = NewHistogramWithScale(entryPointReqDurations, time.Second)
reg.entryPointOpenConnsGauge = entryPointOpenConns
}
if config.AddServicesLabels {
@@ -187,7 +188,7 @@ func initStandardRegistry(config *types.Prometheus) Registry {
}...)
reg.serviceReqsCounter = serviceReqs
reg.serviceReqDurationHistogram = serviceReqDurations
reg.serviceReqDurationHistogram, _ = NewHistogramWithScale(serviceReqDurations, time.Second)
reg.serviceOpenConnsGauge = serviceOpenConns
reg.serviceRetriesCounter = serviceRetries
reg.serviceServerUpGauge = serviceServerUp

View File

@@ -55,14 +55,14 @@ func RegisterStatsd(ctx context.Context, config *types.Statsd) Registry {
if config.AddEntryPointsLabels {
registry.epEnabled = config.AddEntryPointsLabels
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)
}
if config.AddServicesLabels {
registry.svcEnabled = config.AddServicesLabels
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.serviceOpenConnsGauge = statsdClient.NewGauge(statsdOpenConnsName)
registry.serviceServerUpGauge = statsdClient.NewGauge(statsdServerUpName)

View File

@@ -45,8 +45,8 @@ func (f *CommonLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
toLog(entry.Data, "request_Referer", `"-"`, true),
toLog(entry.Data, "request_User-Agent", `"-"`, true),
toLog(entry.Data, RequestCount, defaultValue, true),
toLog(entry.Data, RouterName, defaultValue, true),
toLog(entry.Data, ServiceURL, defaultValue, true),
toLog(entry.Data, RouterName, `"-"`, true),
toLog(entry.Data, ServiceURL, `"-"`, true),
elapsedMillis)
return b.Bytes(), err

View File

@@ -36,7 +36,7 @@ func TestCommonLogFormatter_Format(t *testing.T) {
RouterName: "",
ServiceURL: "",
},
expectedLog: `10.0.0.1 - Client [10/Nov/2009:23:00:00 +0000] "GET /foo http" - - "-" "-" 0 - - 123000ms
expectedLog: `10.0.0.1 - Client [10/Nov/2009:23:00:00 +0000] "GET /foo http" - - "-" "-" 0 "-" "-" 123000ms
`,
},
{

View File

@@ -449,7 +449,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
DefaultMode: "drop",
},
},
expectedLog: `- - - [-] "- - -" - - "testReferer" "testUserAgent" - - - 0ms`,
expectedLog: `- - - [-] "- - -" - - "testReferer" "testUserAgent" - "-" "-" 0ms`,
},
{
desc: "Default mode drop with override",
@@ -464,7 +464,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
},
},
expectedLog: `- - TestUser [-] "- - -" - - "testReferer" "testUserAgent" - - - 0ms`,
expectedLog: `- - TestUser [-] "- - -" - - "testReferer" "testUserAgent" - "-" "-" 0ms`,
},
{
desc: "Default mode drop with header dropped",
@@ -482,7 +482,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
},
},
expectedLog: `- - TestUser [-] "- - -" - - "-" "-" - - - 0ms`,
expectedLog: `- - TestUser [-] "- - -" - - "-" "-" - "-" "-" 0ms`,
},
{
desc: "Default mode drop with header redacted",
@@ -500,7 +500,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
},
},
expectedLog: `- - TestUser [-] "- - -" - - "REDACTED" "REDACTED" - - - 0ms`,
expectedLog: `- - TestUser [-] "- - -" - - "REDACTED" "REDACTED" - "-" "-" 0ms`,
},
{
desc: "Default mode drop with header redacted",
@@ -521,7 +521,7 @@ func TestNewLogHandlerOutputStdout(t *testing.T) {
},
},
},
expectedLog: `- - TestUser [-] "- - -" - - "REDACTED" "testUserAgent" - - - 0ms`,
expectedLog: `- - TestUser [-] "- - -" - - "REDACTED" "testUserAgent" - "-" "-" 0ms`,
},
}

View File

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

View File

@@ -3,6 +3,7 @@ package replacepath
import (
"context"
"net/http"
"net/url"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"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) {
req.Header.Add(ReplacedPathHeader, req.URL.Path)
req.URL.Path = r.path
if req.URL.RawPath == "" {
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()
r.next.ServeHTTP(rw, req)

View File

@@ -3,43 +3,93 @@ package replacepath
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReplacePath(t *testing.T) {
var replacementConfig = dynamic.ReplacePath{
Path: "/replacement-path",
testCases := []struct {
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{
"/example",
"/some/really/long/path",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
var expectedPath, actualHeader, requestURI string
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
var actualPath, actualRawPath, actualHeader, requestURI string
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)
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)
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, path, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader)
assert.Equal(t, expectedPath, requestURI, "Unexpected request URI.")
assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader)
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"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
@@ -49,10 +50,31 @@ func (rp *replacePathRegex) GetTracingInformation() (string, ext.SpanKindEnum) {
}
func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if rp.regexp != nil && len(rp.replacement) > 0 && rp.regexp.MatchString(req.URL.Path) {
req.Header.Add(replacepath.ReplacedPathHeader, req.URL.Path)
req.URL.Path = rp.regexp.ReplaceAllString(req.URL.Path, rp.replacement)
var currentPath string
if req.URL.RawPath == "" {
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()
}
rp.next.ServeHTTP(rw, req)
}

View File

@@ -3,23 +3,24 @@ package replacepathregex
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/middlewares/replacepath"
"github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReplacePathRegex(t *testing.T) {
testCases := []struct {
desc string
path string
config dynamic.ReplacePathRegex
expectedPath string
expectedHeader string
expectsError bool
desc string
path string
config dynamic.ReplacePathRegex
expectedPath string
expectedRawPath string
expectedHeader string
expectsError bool
}{
{
desc: "simple regex",
@@ -28,8 +29,9 @@ func TestReplacePathRegex(t *testing.T) {
Replacement: "/who-am-i/$1",
Regex: `^/whoami/(.*)`,
},
expectedPath: "/who-am-i/and/whoami",
expectedHeader: "/whoami/and/whoami",
expectedPath: "/who-am-i/and/whoami",
expectedRawPath: "/who-am-i/and/whoami",
expectedHeader: "/whoami/and/whoami",
},
{
desc: "simple replace (no regex)",
@@ -38,8 +40,9 @@ func TestReplacePathRegex(t *testing.T) {
Replacement: "/who-am-i",
Regex: `/whoami`,
},
expectedPath: "/who-am-i/and/who-am-i",
expectedHeader: "/whoami/and/whoami",
expectedPath: "/who-am-i/and/who-am-i",
expectedRawPath: "/who-am-i/and/who-am-i",
expectedHeader: "/whoami/and/whoami",
},
{
desc: "no match",
@@ -57,8 +60,9 @@ func TestReplacePathRegex(t *testing.T) {
Replacement: "/downloads/$1-$2",
Regex: `^(?i)/downloads/([^/]+)/([^/]+)$`,
},
expectedPath: "/downloads/src-source.go",
expectedHeader: "/downloads/src/source.go",
expectedPath: "/downloads/src-source.go",
expectedRawPath: "/downloads/src-source.go",
expectedHeader: "/downloads/src/source.go",
},
{
desc: "invalid regular expression",
@@ -70,13 +74,46 @@ func TestReplacePathRegex(t *testing.T) {
expectedPath: "/invalid/regexp/test",
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 {
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) {
actualPath = r.URL.Path
actualRawPath = r.URL.RawPath
actualHeader = r.Header.Get(replacepath.ReplacedPathHeader)
requestURI = r.RequestURI
})
@@ -84,19 +121,29 @@ func TestReplacePathRegex(t *testing.T) {
handler, err := New(context.Background(), next, test.config, "foo-replace-path-regexp")
if test.expectsError {
require.Error(t, err)
} else {
require.NoError(t, err)
return
}
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+test.path, nil)
req.RequestURI = test.path
require.NoError(t, err)
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.")
if test.expectedHeader != "" {
assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", replacepath.ReplacedPathHeader)
}
} else {
assert.Equal(t, actualRawPath, requestURI, "Unexpected request URI.")
}
if test.expectedHeader != "" {
assert.Equal(t, test.expectedHeader, actualHeader, "Unexpected '%s' header.", replacepath.ReplacedPathHeader)
}
})
}

View File

@@ -46,7 +46,7 @@ var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
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"`
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"`
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"`