Introduce trace verbosity config and produce less spans by default

This commit is contained in:
Romain 2025-07-18 15:32:05 +02:00 committed by GitHub
parent 77ef7fe490
commit 8c23eb6833
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
93 changed files with 1005 additions and 524 deletions

View File

@ -319,3 +319,23 @@ and Traefik now keeps them encoded to avoid any ambiguity.
| `/foo/../bar` | PathPrefix(`/bar`) | Match | Match |
| `/foo/%2E%2E/bar` | PathPrefix(`/foo`) | Match | No match |
| `/foo/%2E%2E/bar` | PathPrefix(`/bar`) | No match | Match |
## v3.5.0
### TraceVerbosity on Routers and Entrypoints
Starting with v3.5, a new `traceVerbosity` option is available for both entrypoints and routers.
This option allows you to control the level of detail for tracing spans.
Routers can override the value inherited from their entrypoint.
**Impact:**
- If you rely on tracing, review your configuration to explicitly set the desired verbosity level.
- Existing configurations will default to `minimal` unless overridden, which will result in fewer spans being generated than before.
Possible values are:
- `minimal`: produces a single server span and one client span for each request processed by a router.
- `detailed`: enables the creation of additional spans for each middleware executed for each request processed by a router.
See the updated documentation for [entrypoints](../reference/install-configuration/entrypoints.md) and [dynamic routers](../reference/dynamic-configuration/file.md#observability-options).

View File

@ -169,6 +169,7 @@
- "traefik.http.routers.router0.middlewares=foobar, foobar"
- "traefik.http.routers.router0.observability.accesslogs=true"
- "traefik.http.routers.router0.observability.metrics=true"
- "traefik.http.routers.router0.observability.traceverbosity=foobar"
- "traefik.http.routers.router0.observability.tracing=true"
- "traefik.http.routers.router0.priority=42"
- "traefik.http.routers.router0.rule=foobar"
@ -185,6 +186,7 @@
- "traefik.http.routers.router1.middlewares=foobar, foobar"
- "traefik.http.routers.router1.observability.accesslogs=true"
- "traefik.http.routers.router1.observability.metrics=true"
- "traefik.http.routers.router1.observability.traceverbosity=foobar"
- "traefik.http.routers.router1.observability.tracing=true"
- "traefik.http.routers.router1.priority=42"
- "traefik.http.routers.router1.rule=foobar"

View File

@ -22,8 +22,9 @@
sans = ["foobar", "foobar"]
[http.routers.Router0.observability]
accessLogs = true
tracing = true
metrics = true
tracing = true
traceVerbosity = "foobar"
[http.routers.Router1]
entryPoints = ["foobar", "foobar"]
middlewares = ["foobar", "foobar"]
@ -44,8 +45,9 @@
sans = ["foobar", "foobar"]
[http.routers.Router1.observability]
accessLogs = true
tracing = true
metrics = true
tracing = true
traceVerbosity = "foobar"
[http.services]
[http.services.Service01]
[http.services.Service01.failover]

View File

@ -27,8 +27,9 @@ http:
- foobar
observability:
accessLogs: true
tracing: true
metrics: true
tracing: true
traceVerbosity: foobar
Router1:
entryPoints:
- foobar
@ -54,8 +55,9 @@ http:
- foobar
observability:
accessLogs: true
tracing: true
metrics: true
tracing: true
traceVerbosity: foobar
services:
Service01:
failover:

View File

@ -92,10 +92,21 @@ spec:
More info: https://doc.traefik.io/traefik/v3.5/routing/routers/#observability
properties:
accessLogs:
description: AccessLogs enables access logs for this router.
type: boolean
metrics:
description: Metrics enables metrics for this router.
type: boolean
traceVerbosity:
default: minimal
description: TraceVerbosity defines the verbosity level
of the tracing for this router.
enum:
- minimal
- detailed
type: string
tracing:
description: Tracing enables tracing for this router.
type: boolean
type: object
priority:

View File

@ -199,6 +199,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/routers/Router0/middlewares/1` | `foobar` |
| `traefik/http/routers/Router0/observability/accessLogs` | `true` |
| `traefik/http/routers/Router0/observability/metrics` | `true` |
| `traefik/http/routers/Router0/observability/traceVerbosity` | `foobar` |
| `traefik/http/routers/Router0/observability/tracing` | `true` |
| `traefik/http/routers/Router0/priority` | `42` |
| `traefik/http/routers/Router0/rule` | `foobar` |
@ -218,6 +219,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/routers/Router1/middlewares/1` | `foobar` |
| `traefik/http/routers/Router1/observability/accessLogs` | `true` |
| `traefik/http/routers/Router1/observability/metrics` | `true` |
| `traefik/http/routers/Router1/observability/traceVerbosity` | `foobar` |
| `traefik/http/routers/Router1/observability/tracing` | `true` |
| `traefik/http/routers/Router1/priority` | `42` |
| `traefik/http/routers/Router1/rule` | `foobar` |

View File

@ -92,10 +92,21 @@ spec:
More info: https://doc.traefik.io/traefik/v3.5/routing/routers/#observability
properties:
accessLogs:
description: AccessLogs enables access logs for this router.
type: boolean
metrics:
description: Metrics enables metrics for this router.
type: boolean
traceVerbosity:
default: minimal
description: TraceVerbosity defines the verbosity level
of the tracing for this router.
enum:
- minimal
- detailed
type: string
tracing:
description: Tracing enables tracing for this router.
type: boolean
type: object
priority:

View File

@ -83,39 +83,40 @@ additionalArguments:
## Configuration Options
| Field | Description | Default | Required |
|:-----------------|:--------|:--------|:---------|
| `address` | Define the port, and optionally the hostname, on which to listen for incoming connections and packets.<br /> It also defines the protocol to use (TCP or UDP).<br /> If no protocol is specified, the default is TCP. The format is:`[host]:port[/tcp\|/udp] | - | Yes |
| `asDefault` | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| `forwardedHeaders.trustedIPs` | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| `forwardedHeaders.insecure` | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| `http.redirections.`<br />`entryPoint.to` | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
| `http.redirections.`<br />`entryPoint.scheme` | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
| `http.redirections.`<br />`entryPoint.permanent` | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
| `http.redirections.`<br />`entryPoint.priority` | Default priority applied to the routers attached to the `entryPoint`.| MaxInt32-1 (2147483646) | No |
| `http.encodeQuerySemicolons` | Enable query semicolons encoding. <br /> Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik. <br /> When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.<br /> More information [here](#encodequerysemicolons). | false | No |
| `http.sanitizePath` | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | false | No |
| `http.middlewares` | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. <br />More information [here](#httpmiddlewares). | - | No |
| `http.tls` | Enable TLS on every router attached to the `entryPoint`. <br /> If no certificate are set, a default self-signed certificate is generates by Traefik. <br /> We recommend to not use self signed certificates in production.| - | No |
| `http.tls.options` | Apply TLS options on every router attached to the `entryPoint`. <br /> The TLS options can be overidden per router. <br /> More information in the [dedicated section](../../routing/providers/kubernetes-crd.md#kind-tlsoption). | - | No |
| `http.tls.certResolver` | Apply a certificate resolver on every router attached to the `entryPoint`. <br /> The TLS options can be overidden per router. <br /> More information in the [dedicated section](../install-configuration/tls/certificate-resolvers/overview.md). | - | No |
| `http2.maxConcurrentStreams` | Set the number of concurrent streams per connection that each client is allowed to initiate. <br /> The value must be greater than zero. | 250 | No |
| `http3` | Enable HTTP/3 protocol on the `entryPoint`. <br /> HTTP/3 requires a TCP `entryPoint`. as HTTP/3 always starts as a TCP connection that then gets upgraded to UDP. In most scenarios, this `entryPoint` is the same as the one used for TLS traffic.<br /> More information [here](#http3. | - | No |
| `http3.advertisedPort` | Set the UDP port to advertise as the HTTP/3 authority. <br /> It defaults to the entryPoint's address port. <br /> It can be used to override the authority in the `alt-svc` header, for example if the public facing port is different from where Traefik is listening. | - | No |
| `observability.accessLogs` | Defines whether a router attached to this EntryPoint produces access-logs by default. Nonetheless, a router defining its own observability configuration will opt-out from this default. | true | No |
| `observability.metrics` | Defines whether a router attached to this EntryPoint produces metrics by default. Nonetheless, a router defining its own observability configuration will opt-out from this default. | true | No |
| `observability.tracing` | Defines whether a router attached to this EntryPoint produces traces by default. Nonetheless, a router defining its own observability configuration will opt-out from this default. | true | No |
| `proxyProtocol.trustedIPs` | Enable PROXY protocol with Trusted IPs. <br /> Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2. <br /> If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers. <br /> If the PROXY protocol header is passed, then the version is determined automatically.<br /> More information [here](#proxyprotocol-and-load-balancers). | - | No |
| `proxyProtocol.insecure` | Enable PROXY protocol trusting every incoming connection. <br /> Every remote client address will be replaced (`trustedIPs`) won't have any effect). <br /> Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2. <br /> If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers. <br /> If the PROXY protocol header is passed, then the version is determined automatically.<br />We recommend to use this option only for tests purposes, not in production.<br /> More information [here](#proxyprotocol-and-load-balancers). | - | No |
| `reusePort` | Enable `entryPoints` from the same or different processes listening on the same TCP/UDP port by utilizing the `SO_REUSEPORT` socket option. <br /> It also allows the kernel to act like a load balancer to distribute incoming connections between entry points..<br /> More information [here](#reuseport). | false | No |
| `transport.`<br />`respondingTimeouts.`<br />`readTimeout` | Set the timeouts for incoming requests to the Traefik instance. This is the maximum duration for reading the entire request, including the body. Setting them has no effect for UDP `entryPoints`.<br /> If zero, no timeout exists. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds. | 60s (seconds) | No |
| `transport.`<br />`respondingTimeouts.`<br />`writeTimeout` | Maximum duration before timing out writes of the response. <br /> It covers the time from the end of the request header read to the end of the response write. <br /> If zero, no timeout exists. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds. | 0s (seconds) | No |
| `transport.`<br />`respondingTimeouts.`<br />`idleTimeout` | Maximum duration an idle (keep-alive) connection will remain idle before closing itself. <br /> If zero, no timeout exists <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds | 180s (seconds) | No |
| `transport.`<br />`lifeCycle.`<br />`graceTimeOut` | Set the duration to give active requests a chance to finish before Traefik stops. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds <br /> In this time frame no new requests are accepted. | 10s (seconds) | No |
| `transport.`<br />`lifeCycle.`<br />`requestAcceptGraceTimeout` | Set the duration to keep accepting requests prior to initiating the graceful termination period (as defined by the `transportlifeCycle.graceTimeOut` option). <br /> This option is meant to give downstream load-balancers sufficient time to take Traefik out of rotation. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds | 0s (seconds) | No |
| `transport.`<br />`keepAliveMaxRequests` | Set the maximum number of requests Traefik can handle before sending a `Connection: Close` header to the client (for HTTP2, Traefik sends a GOAWAY). <br /> Zero means no limit. | 0 | No |
| `transport.`<br />`keepAliveMaxTime` | Set the maximum duration Traefik can handle requests before sending a `Connection: Close` header to the client (for HTTP2, Traefik sends a GOAWAY). Zero means no limit. | 0s (seconds) | No |
| `udp.timeout` | Define how long to wait on an idle session before releasing the related resources. <br />The Timeout value must be greater than zero. | 3s (seconds)| No |
| Field | Description | Default | Required |
|:----------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------|:---------|
| `address` | Define the port, and optionally the hostname, on which to listen for incoming connections and packets.<br /> It also defines the protocol to use (TCP or UDP).<br /> If no protocol is specified, the default is TCP. The format is:`[host]:port[/tcp\|/udp] | - | Yes |
| `asDefault` | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
| `forwardedHeaders.trustedIPs` | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
| `forwardedHeaders.insecure` | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
| `http.redirections.`<br />`entryPoint.to` | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one. <br /> The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes |
| `http.redirections.`<br />`entryPoint.scheme` | The target scheme to use for (permanent) redirection of all incoming requests. | https | No |
| `http.redirections.`<br />`entryPoint.permanent` | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme. <br /> The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No |
| `http.redirections.`<br />`entryPoint.priority` | Default priority applied to the routers attached to the `entryPoint`. | MaxInt32-1 (2147483646) | No |
| `http.encodeQuerySemicolons` | Enable query semicolons encoding. <br /> Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik. <br /> When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.<br /> More information [here](#encodequerysemicolons). | false | No |
| `http.sanitizePath` | Defines whether to enable the request path sanitization.<br /> More information [here](#sanitizepath). | false | No |
| `http.middlewares` | Set the list of middlewares that are prepended by default to the list of middlewares of each router associated to the named entry point. <br />More information [here](#httpmiddlewares). | - | No |
| `http.tls` | Enable TLS on every router attached to the `entryPoint`. <br /> If no certificate are set, a default self-signed certificate is generates by Traefik. <br /> We recommend to not use self signed certificates in production. | - | No |
| `http.tls.options` | Apply TLS options on every router attached to the `entryPoint`. <br /> The TLS options can be overidden per router. <br /> More information in the [dedicated section](../../routing/providers/kubernetes-crd.md#kind-tlsoption). | - | No |
| `http.tls.certResolver` | Apply a certificate resolver on every router attached to the `entryPoint`. <br /> The TLS options can be overidden per router. <br /> More information in the [dedicated section](../install-configuration/tls/certificate-resolvers/overview.md). | - | No |
| `http2.maxConcurrentStreams` | Set the number of concurrent streams per connection that each client is allowed to initiate. <br /> The value must be greater than zero. | 250 | No |
| `http3` | Enable HTTP/3 protocol on the `entryPoint`. <br /> HTTP/3 requires a TCP `entryPoint`. as HTTP/3 always starts as a TCP connection that then gets upgraded to UDP. In most scenarios, this `entryPoint` is the same as the one used for TLS traffic.<br /> More information [here](#http3. | - | No |
| `http3.advertisedPort` | Set the UDP port to advertise as the HTTP/3 authority. <br /> It defaults to the entryPoint's address port. <br /> It can be used to override the authority in the `alt-svc` header, for example if the public facing port is different from where Traefik is listening. | - | No |
| `observability.accessLogs` | Defines whether a router attached to this EntryPoint produces access-logs by default. Nonetheless, a router defining its own observability configuration will opt-out from this default. | true | No |
| `observability.metrics` | Defines whether a router attached to this EntryPoint produces metrics by default. Nonetheless, a router defining its own observability configuration will opt-out from this default. | true | No |
| `observability.tracing` | Defines whether a router attached to this EntryPoint produces traces by default. Nonetheless, a router defining its own observability configuration will opt-out from this default. | true | No |
| `observability.traceVerbosity` | Defines the tracing verbosity level for routers attached to this EntryPoint. Possible values: `minimal` (default), `detailed`. Routers can override this value in their own observability configuration. <br /> More information [here](#traceverbosity). | minimal | No |
| `proxyProtocol.trustedIPs` | Enable PROXY protocol with Trusted IPs. <br /> Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2. <br /> If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers. <br /> If the PROXY protocol header is passed, then the version is determined automatically.<br /> More information [here](#proxyprotocol-and-load-balancers). | - | No |
| `proxyProtocol.insecure` | Enable PROXY protocol trusting every incoming connection. <br /> Every remote client address will be replaced (`trustedIPs`) won't have any effect). <br /> Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2. <br /> If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers. <br /> If the PROXY protocol header is passed, then the version is determined automatically.<br />We recommend to use this option only for tests purposes, not in production.<br /> More information [here](#proxyprotocol-and-load-balancers). | - | No |
| `reusePort` | Enable `entryPoints` from the same or different processes listening on the same TCP/UDP port by utilizing the `SO_REUSEPORT` socket option. <br /> It also allows the kernel to act like a load balancer to distribute incoming connections between entry points.<br /> More information [here](#reuseport). | false | No |
| `transport.`<br />`respondingTimeouts.`<br />`readTimeout` | Set the timeouts for incoming requests to the Traefik instance. This is the maximum duration for reading the entire request, including the body. Setting them has no effect for UDP `entryPoints`.<br /> If zero, no timeout exists. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds. | 60s (seconds) | No |
| `transport.`<br />`respondingTimeouts.`<br />`writeTimeout` | Maximum duration before timing out writes of the response. <br /> It covers the time from the end of the request header read to the end of the response write. <br /> If zero, no timeout exists. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds. | 0s (seconds) | No |
| `transport.`<br />`respondingTimeouts.`<br />`idleTimeout` | Maximum duration an idle (keep-alive) connection will remain idle before closing itself. <br /> If zero, no timeout exists <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds | 180s (seconds) | No |
| `transport.`<br />`lifeCycle.`<br />`graceTimeOut` | Set the duration to give active requests a chance to finish before Traefik stops. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds <br /> In this time frame no new requests are accepted. | 10s (seconds) | No |
| `transport.`<br />`lifeCycle.`<br />`requestAcceptGraceTimeout` | Set the duration to keep accepting requests prior to initiating the graceful termination period (as defined by the `transportlifeCycle.graceTimeOut` option). <br /> This option is meant to give downstream load-balancers sufficient time to take Traefik out of rotation. <br />Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).<br />If no units are provided, the value is parsed assuming seconds | 0s (seconds) | No |
| `transport.`<br />`keepAliveMaxRequests` | Set the maximum number of requests Traefik can handle before sending a `Connection: Close` header to the client (for HTTP2, Traefik sends a GOAWAY). <br /> Zero means no limit. | 0 | No |
| `transport.`<br />`keepAliveMaxTime` | Set the maximum duration Traefik can handle requests before sending a `Connection: Close` header to the client (for HTTP2, Traefik sends a GOAWAY). Zero means no limit. | 0s (seconds) | No |
| `udp.timeout` | Define how long to wait on an idle session before releasing the related resources. <br />The Timeout value must be greater than zero. | 3s (seconds) | No |
### asDefault
@ -271,3 +272,13 @@ Use the `reusePort` option with the other option `transport.lifeCycle.gracetimeo
to do
canary deployments against Traefik itself. Like upgrading Traefik version
or reloading the static configuration without any service downtime.
#### Trace Verbosity
`observability.traceVerbosity` defines the tracing verbosity level for routers attached to this EntryPoint.
Routers can override this value in their own observability configuration.
Possible values are:
- `minimal`: produces a single server span and one client span for each request processed by a router.
- `detailed`: enables the creation of additional spans for each middleware executed for each request processed by a router.

View File

@ -36,6 +36,7 @@ http:
metrics: false
accessLogs: false
tracing: false
traceVerbosity: detailed
```
```yaml tab="Structured (TOML)"
@ -47,6 +48,7 @@ http:
metrics = false
accessLogs = false
tracing = false
traceVerbosity = "detailed"
```
```yaml tab="Labels"
@ -56,6 +58,7 @@ labels:
- "traefik.http.routers.my-router.observability.metrics=false"
- "traefik.http.routers.my-router.observability.accessLogs=false"
- "traefik.http.routers.my-router.observability.tracing=false"
- "traefik.http.routers.my-router.observability.traceVerbosity=detailed"
```
```json tab="Tags"
@ -66,15 +69,26 @@ labels:
"traefik.http.routers.my-router.service=service-foo",
"traefik.http.routers.my-router.observability.metrics=false",
"traefik.http.routers.my-router.observability.accessLogs=false",
"traefik.http.routers.my-router.observability.tracing=false"
"traefik.http.routers.my-router.observability.tracing=false",
"traefik.http.routers.my-router.observability.traceVerbosity=detailed"
]
}
```
## Configuration Options
| Field | Description | Default | Required |
|:------|:------------|:--------|:---------|
| `accessLogs` | The `accessLogs` option controls whether the router will produce access-logs. | `true` | No |
| `metrics` | The `metrics` option controls whether the router will produce metrics. | `true` | No |
| `tracing` | The `tracing` option controls whether the router will produce traces. | `true` | No |
| Field | Description | Default | Required |
|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------|:---------|
| `accessLogs` | The `accessLogs` option controls whether the router will produce access-logs. | `true` | No |
| `metrics` | The `metrics` option controls whether the router will produce metrics. | `true` | No |
| `tracing` | The `tracing` option controls whether the router will produce traces. | `true` | No |
| `traceVerbosity` | The `traceVerbosity` option controls the tracing verbosity level for the router. Possible values: `minimal` (default), `detailed`. If not set, the value is inherited from the entryPoint. | `minimal` | No |
#### traceVerbosity
`observability.traceVerbosity` defines the tracing verbosity level for the router.
Possible values are:
- `minimal`: produces a single server span and one client span for each request processed by a router.
- `detailed`: enables the creation of additional spans for each middleware executed for each request processed by a router.

View File

@ -283,13 +283,16 @@ HTTP/3 configuration. (Default: ```false```)
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
`--entrypoints.<name>.observability.accesslogs`:
(Default: ```true```)
Enables access-logs for this entryPoint. (Default: ```true```)
`--entrypoints.<name>.observability.metrics`:
(Default: ```true```)
Enables metrics for this entryPoint. (Default: ```true```)
`--entrypoints.<name>.observability.traceverbosity`:
Defines the tracing verbosity level for this entryPoint. (Default: ```minimal```)
`--entrypoints.<name>.observability.tracing`:
(Default: ```true```)
Enables tracing for this entryPoint. (Default: ```true```)
`--entrypoints.<name>.proxyprotocol`:
Proxy-Protocol configuration. (Default: ```false```)

View File

@ -283,13 +283,16 @@ Subject alternative names.
Default TLS options for the routers linked to the entry point.
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_ACCESSLOGS`:
(Default: ```true```)
Enables access-logs for this entryPoint. (Default: ```true```)
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_METRICS`:
(Default: ```true```)
Enables metrics for this entryPoint. (Default: ```true```)
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_TRACEVERBOSITY`:
Defines the tracing verbosity level for this entryPoint. (Default: ```minimal```)
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_TRACING`:
(Default: ```true```)
Enables tracing for this entryPoint. (Default: ```true```)
`TRAEFIK_ENTRYPOINTS_<NAME>_PROXYPROTOCOL`:
Proxy-Protocol configuration. (Default: ```false```)

View File

@ -80,8 +80,9 @@
timeout = "42s"
[entryPoints.EntryPoint0.observability]
accessLogs = true
tracing = true
metrics = true
tracing = true
traceVerbosity = "foobar"
[providers]
providersThrottleDuration = "42s"

View File

@ -94,8 +94,9 @@ entryPoints:
timeout: 42s
observability:
accessLogs: true
tracing: true
metrics: true
tracing: true
traceVerbosity: foobar
providers:
providersThrottleDuration: 42s
docker:

View File

@ -648,25 +648,6 @@ func (s *AccessLogSuite) TestAccessLogDisabledForInternals() {
require.Equal(s.T(), 0, count)
// Make some requests on the custom ping router in error.
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8010/ping-error", nil)
require.NoError(s.T(), err)
req.Host = "ping-error.docker.local"
err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusUnauthorized), try.BodyContains("X-Forwarded-Host: ping-error.docker.local"))
require.NoError(s.T(), err)
err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusUnauthorized), try.BodyContains("X-Forwarded-Host: ping-error.docker.local"))
require.NoError(s.T(), err)
// Here we verify that the remove of observability doesn't break the metrics for the error page service.
req, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8080/metrics", nil)
require.NoError(s.T(), err)
err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.BodyContains("service3"))
require.NoError(s.T(), err)
err = try.Request(req, 500*time.Millisecond, try.StatusCodeIs(http.StatusOK), try.BodyNotContains("service=\"ping"))
require.NoError(s.T(), err)
// Verify no other Traefik problems.
s.checkNoOtherTraefikProblems()
}

View File

@ -92,10 +92,21 @@ spec:
More info: https://doc.traefik.io/traefik/v3.5/routing/routers/#observability
properties:
accessLogs:
description: AccessLogs enables access logs for this router.
type: boolean
metrics:
description: Metrics enables metrics for this router.
type: boolean
traceVerbosity:
default: minimal
description: TraceVerbosity defines the verbosity level
of the tracing for this router.
enum:
- minimal
- detailed
type: string
tracing:
description: Tracing enables tracing for this router.
type: boolean
type: object
priority:

View File

@ -14,6 +14,10 @@
[entryPoints]
[entryPoints.web]
address = ":8000"
[entryPoints.web.observability]
traceVerbosity = "detailed"
[entryPoints.web-minimal]
address = ":8001"
# Adding metrics to confirm that there is no wrong interaction with tracing.
[metrics]
@ -44,9 +48,13 @@
## dynamic configuration ##
[http.routers]
[http.routers.routerBasicMinimal]
Service = "service0"
Rule = "Path(`/basic-minimal`)"
[http.routers.routerBasicMinimal.observability]
traceVerbosity = "minimal"
[http.routers.router0]
Service = "service0"
Middlewares = []
Rule = "Path(`/basic`)"
[http.routers.router1]
Service = "service1"

View File

@ -101,13 +101,3 @@ services:
traefik.http.routers.ping.entryPoints: ping
traefik.http.routers.ping.rule: PathPrefix(`/ping`)
traefik.http.routers.ping.service: ping@internal
traefik.http.routers.ping-error.entryPoints: ping
traefik.http.routers.ping-error.rule: PathPrefix(`/ping-error`)
traefik.http.routers.ping-error.middlewares: errors, basicauth
traefik.http.routers.ping-error.service: ping@internal
traefik.http.middlewares.basicauth.basicauth.users: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"
traefik.http.middlewares.errors.errors.status: 401
traefik.http.middlewares.errors.errors.service: service3
traefik.http.middlewares.errors.errors.query: /
traefik.http.services.service3.loadbalancer.server.port: 80

View File

@ -14,8 +14,9 @@
"tls": {},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +50,9 @@
},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -67,8 +69,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -89,8 +92,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -100,7 +104,13 @@
},
"middlewares": {
"compressor@consul": {
"compress": {},
"compress": {
"encodings": [
"gzip",
"br",
"zstd"
]
},
"status": "enabled",
"usedBy": [
"Router0@consul"
@ -173,6 +183,7 @@
"mirror@consul": {
"mirroring": {
"service": "simplesvc",
"mirrorBody": true,
"maxBodySize": -1,
"mirrors": [
{

View File

@ -9,8 +9,9 @@
"priority": 18,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -29,8 +30,9 @@
},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [

View File

@ -9,8 +9,9 @@
"priority": 18,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -29,8 +30,9 @@
},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +51,9 @@
"priority": 46,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -66,8 +69,9 @@
"priority": 38,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -83,8 +87,9 @@
"priority": 50,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -100,8 +105,9 @@
"priority": 35,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"error": [
"the service \"other-ns-wrr3@kubernetescrd\" does not exist"

View File

@ -14,8 +14,9 @@
"tls": {},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +50,9 @@
},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -67,8 +69,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -89,8 +92,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -100,7 +104,13 @@
},
"middlewares": {
"compressor@etcd": {
"compress": {},
"compress": {
"encodings": [
"gzip",
"br",
"zstd"
]
},
"status": "enabled",
"usedBy": [
"Router0@etcd"
@ -173,6 +183,7 @@
"mirror@etcd": {
"mirroring": {
"service": "simplesvc",
"mirrorBody": true,
"maxBodySize": -1,
"mirrors": [
{

View File

@ -10,8 +10,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -32,8 +33,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -50,8 +52,9 @@
"priority": 100008,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -69,8 +72,9 @@
"tls": {},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [

View File

@ -10,8 +10,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -32,8 +33,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +51,9 @@
"priority": 44,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [

View File

@ -10,8 +10,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -32,8 +33,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +51,9 @@
"priority": 50,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -66,8 +69,9 @@
"priority": 44,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -83,8 +87,9 @@
"priority": 47,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -100,8 +105,9 @@
"priority": 47,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [

View File

@ -10,8 +10,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -32,8 +33,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [

View File

@ -10,8 +10,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -32,8 +33,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +51,9 @@
"priority": 47,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [

View File

@ -14,8 +14,9 @@
"tls": {},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +50,9 @@
},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -67,8 +69,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -89,8 +92,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -100,7 +104,13 @@
},
"middlewares": {
"compressor@redis": {
"compress": {},
"compress": {
"encodings": [
"gzip",
"br",
"zstd"
]
},
"status": "enabled",
"usedBy": [
"Router0@redis"
@ -173,6 +183,7 @@
"mirror@redis": {
"mirroring": {
"service": "simplesvc",
"mirrorBody": true,
"maxBodySize": -1,
"mirrors": [
{
@ -244,6 +255,7 @@
"url": "http://10.0.1.3:8889"
}
],
"strategy": "wrr",
"passHostHeader": true,
"responseForwarding": {
"flushInterval": "100ms"

View File

@ -14,8 +14,9 @@
"tls": {},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -49,8 +50,9 @@
},
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -67,8 +69,9 @@
"priority": 9223372036854775806,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -89,8 +92,9 @@
"priority": 9223372036854775805,
"observability": {
"accessLogs": true,
"metrics": true,
"tracing": true,
"metrics": true
"traceVerbosity": "minimal"
},
"status": "enabled",
"using": [
@ -100,7 +104,13 @@
},
"middlewares": {
"compressor@zookeeper": {
"compress": {},
"compress": {
"encodings": [
"gzip",
"br",
"zstd"
]
},
"status": "enabled",
"usedBy": [
"Router0@zookeeper"
@ -173,6 +183,7 @@
"mirror@zookeeper": {
"mirroring": {
"service": "simplesvc",
"mirrorBody": true,
"maxBodySize": -1,
"mirrors": [
{

View File

@ -77,6 +77,104 @@ func (s *TracingSuite) TearDownTest() {
s.composeStop("tempo")
}
func (s *TracingSuite) TestOpenTelemetryBasic_HTTP_router_minimalVerbosity() {
file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{
WhoamiIP: s.whoamiIP,
WhoamiPort: s.whoamiPort,
IP: s.otelCollectorIP,
IsHTTP: true,
})
s.traefikCmd(withConfigFile(file))
// wait for traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("basic-auth"))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8000/basic-minimal", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
require.NoError(s.T(), err)
contains := []map[string]string{
{
"batches.0.scopeSpans.0.scope.name": "github.com/traefik/traefik",
"batches.0.scopeSpans.0.spans.0.name": "ReverseProxy",
"batches.0.scopeSpans.0.spans.0.kind": "SPAN_KIND_CLIENT",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"http.request.method\").value.stringValue": "GET",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"network.protocol.version\").value.stringValue": "1.1",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"url.full\").value.stringValue": fmt.Sprintf("http://%s/basic-minimal", net.JoinHostPort(s.whoamiIP, "80")),
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"network.peer.address\").value.stringValue": s.whoamiIP,
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"network.peer.port\").value.intValue": "80",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"server.address\").value.stringValue": s.whoamiIP,
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"server.port\").value.intValue": "80",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"http.response.status_code\").value.intValue": "200",
"batches.0.scopeSpans.0.spans.1.name": "EntryPoint",
"batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_SERVER",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"entry_point\").value.stringValue": "web",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.request.method\").value.stringValue": "GET",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"url.path\").value.stringValue": "/basic-minimal",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"url.query\").value.stringValue": "",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8000",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.response.status_code\").value.intValue": "200",
},
}
s.checkTraceContent(contains)
}
func (s *TracingSuite) TestOpenTelemetryBasic_HTTP_entrypoint_minimalVerbosity() {
file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{
WhoamiIP: s.whoamiIP,
WhoamiPort: s.whoamiPort,
IP: s.otelCollectorIP,
IsHTTP: true,
})
s.traefikCmd(withConfigFile(file))
// wait for traefik
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", time.Second, try.BodyContains("basic-auth"))
require.NoError(s.T(), err)
err = try.GetRequest("http://127.0.0.1:8001/basic", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
require.NoError(s.T(), err)
contains := []map[string]string{
{
"batches.0.scopeSpans.0.scope.name": "github.com/traefik/traefik",
"batches.0.scopeSpans.0.spans.0.name": "ReverseProxy",
"batches.0.scopeSpans.0.spans.0.kind": "SPAN_KIND_CLIENT",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"http.request.method\").value.stringValue": "GET",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"network.protocol.version\").value.stringValue": "1.1",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"url.full\").value.stringValue": fmt.Sprintf("http://%s/basic", net.JoinHostPort(s.whoamiIP, "80")),
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"network.peer.address\").value.stringValue": s.whoamiIP,
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"network.peer.port\").value.intValue": "80",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"server.address\").value.stringValue": s.whoamiIP,
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"server.port\").value.intValue": "80",
"batches.0.scopeSpans.0.spans.0.attributes.#(key=\"http.response.status_code\").value.intValue": "200",
"batches.0.scopeSpans.0.spans.1.name": "EntryPoint",
"batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_SERVER",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"entry_point\").value.stringValue": "web-minimal",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.request.method\").value.stringValue": "GET",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"url.path\").value.stringValue": "/basic",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"url.query\").value.stringValue": "",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"user_agent.original\").value.stringValue": "Go-http-client/1.1",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"server.address\").value.stringValue": "127.0.0.1:8001",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"network.peer.address\").value.stringValue": "127.0.0.1",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.response.status_code\").value.intValue": "200",
},
}
s.checkTraceContent(contains)
}
func (s *TracingSuite) TestOpenTelemetryBasic_HTTP() {
file := s.adaptFile("fixtures/tracing/simple-opentelemetry.toml", TracingTemplate{
WhoamiIP: s.whoamiIP,
@ -121,7 +219,7 @@ func (s *TracingSuite) TestOpenTelemetryBasic_HTTP() {
"batches.0.scopeSpans.0.spans.3.name": "Router",
"batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.service.name\").value.stringValue": "service0@file",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.router.name\").value.stringValue": "router0@file",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router0@file",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.route\").value.stringValue": "Path(`/basic`)",
"batches.0.scopeSpans.0.spans.4.name": "Metrics",
@ -189,7 +287,7 @@ func (s *TracingSuite) TestOpenTelemetryBasic_gRPC() {
"batches.0.scopeSpans.0.spans.3.name": "Router",
"batches.0.scopeSpans.0.spans.3.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.service.name\").value.stringValue": "service0@file",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.router.name\").value.stringValue": "router0@file",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router0@file",
"batches.0.scopeSpans.0.spans.3.attributes.#(key=\"http.route\").value.stringValue": "Path(`/basic`)",
"batches.0.scopeSpans.0.spans.4.name": "Metrics",
@ -251,7 +349,7 @@ func (s *TracingSuite) TestOpenTelemetryRateLimit() {
"batches.0.scopeSpans.0.spans.1.name": "Router",
"batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.service.name\").value.stringValue": "service1@file",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.router.name\").value.stringValue": "router1@file",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router1@file",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.route\").value.stringValue": "Path(`/ratelimit`)",
"batches.0.scopeSpans.0.spans.2.name": "Metrics",
@ -299,7 +397,7 @@ func (s *TracingSuite) TestOpenTelemetryRateLimit() {
"batches.0.scopeSpans.0.spans.4.name": "Router",
"batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.service.name\").value.stringValue": "service1@file",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.router.name\").value.stringValue": "router1@file",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router1@file",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"http.route\").value.stringValue": "Path(`/ratelimit`)",
"batches.0.scopeSpans.0.spans.5.name": "Metrics",
@ -423,7 +521,7 @@ func (s *TracingSuite) TestOpenTelemetryRetry() {
"batches.0.scopeSpans.0.spans.12.name": "Router",
"batches.0.scopeSpans.0.spans.12.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.12.attributes.#(key=\"traefik.service.name\").value.stringValue": "service2@file",
"batches.0.scopeSpans.0.spans.12.attributes.#(key=\"traefik.router.name\").value.stringValue": "router2@file",
"batches.0.scopeSpans.0.spans.12.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router2@file",
"batches.0.scopeSpans.0.spans.13.name": "Metrics",
"batches.0.scopeSpans.0.spans.13.kind": "SPAN_KIND_INTERNAL",
@ -475,7 +573,7 @@ func (s *TracingSuite) TestOpenTelemetryAuth() {
"batches.0.scopeSpans.0.spans.1.name": "Router",
"batches.0.scopeSpans.0.spans.1.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.service.name\").value.stringValue": "service3@file",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.router.name\").value.stringValue": "router3@file",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router3@file",
"batches.0.scopeSpans.0.spans.1.attributes.#(key=\"http.route\").value.stringValue": "Path(`/auth`)",
"batches.0.scopeSpans.0.spans.2.name": "Metrics",
@ -532,7 +630,7 @@ func (s *TracingSuite) TestOpenTelemetryAuthWithRetry() {
"batches.0.scopeSpans.0.spans.2.name": "Router",
"batches.0.scopeSpans.0.spans.2.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.service.name\").value.stringValue": "service4@file",
"batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.router.name\").value.stringValue": "router4@file",
"batches.0.scopeSpans.0.spans.2.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router4@file",
"batches.0.scopeSpans.0.spans.2.attributes.#(key=\"http.route\").value.stringValue": "Path(`/retry-auth`)",
"batches.0.scopeSpans.0.spans.3.name": "Metrics",
@ -601,7 +699,7 @@ func (s *TracingSuite) TestOpenTelemetrySafeURL() {
"batches.0.scopeSpans.0.spans.4.name": "Router",
"batches.0.scopeSpans.0.spans.4.kind": "SPAN_KIND_INTERNAL",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.service.name\").value.stringValue": "service3@file",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.router.name\").value.stringValue": "router3@file",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"traefik.router.name\").value.stringValue": "web-router3@file",
"batches.0.scopeSpans.0.spans.4.attributes.#(key=\"http.route\").value.stringValue": "Path(`/auth`)",
"batches.0.scopeSpans.0.spans.5.name": "Metrics",

View File

@ -88,9 +88,21 @@ type RouterTLSConfig struct {
// RouterObservabilityConfig holds the observability configuration for a router.
type RouterObservabilityConfig struct {
// AccessLogs enables access logs for this router.
AccessLogs *bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
// Metrics enables metrics for this router.
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
// Tracing enables tracing for this router.
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
// TraceVerbosity defines the verbosity level of the tracing for this router.
// +kubebuilder:validation:Enum=minimal;detailed
// +kubebuilder:default=minimal
TraceVerbosity types.TracingVerbosity `json:"traceVerbosity,omitempty" toml:"traceVerbosity,omitempty" yaml:"traceVerbosity,omitempty" export:"true"`
}
// SetDefaults Default values for a RouterObservabilityConfig.
func (r *RouterObservabilityConfig) SetDefaults() {
r.TraceVerbosity = types.MinimalVerbosity
}
// +k8s:deepcopy-gen=true

View File

@ -1335,13 +1335,13 @@ func (in *RouterObservabilityConfig) DeepCopyInto(out *RouterObservabilityConfig
*out = new(bool)
**out = **in
}
if in.Tracing != nil {
in, out := &in.Tracing, &out.Tracing
if in.Metrics != nil {
in, out := &in.Metrics, &out.Metrics
*out = new(bool)
**out = **in
}
if in.Metrics != nil {
in, out := &in.Metrics, &out.Metrics
if in.Tracing != nil {
in, out := &in.Tracing, &out.Tracing
*out = new(bool)
**out = **in
}

View File

@ -26,9 +26,10 @@ const (
type Configuration struct {
Routers map[string]*RouterInfo `json:"routers,omitempty"`
Middlewares map[string]*MiddlewareInfo `json:"middlewares,omitempty"`
TCPMiddlewares map[string]*TCPMiddlewareInfo `json:"tcpMiddlewares,omitempty"`
Services map[string]*ServiceInfo `json:"services,omitempty"`
Models map[string]*dynamic.Model `json:"-"`
TCPRouters map[string]*TCPRouterInfo `json:"tcpRouters,omitempty"`
TCPMiddlewares map[string]*TCPMiddlewareInfo `json:"tcpMiddlewares,omitempty"`
TCPServices map[string]*TCPServiceInfo `json:"tcpServices,omitempty"`
UDPRouters map[string]*UDPRouterInfo `json:"udpRouters,omitempty"`
UDPServices map[string]*UDPServiceInfo `json:"udpServices,omitempty"`
@ -66,6 +67,8 @@ func NewConfig(conf dynamic.Configuration) *Configuration {
runtimeConfig.Middlewares[k] = &MiddlewareInfo{Middleware: v, Status: StatusEnabled}
}
}
runtimeConfig.Models = conf.HTTP.Models
}
if conf.TCP != nil {

View File

@ -165,15 +165,17 @@ func (u *UDPConfig) SetDefaults() {
// ObservabilityConfig holds the observability configuration for an entry point.
type ObservabilityConfig struct {
AccessLogs *bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Tracing *bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
Metrics *bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
AccessLogs *bool `description:"Enables access-logs for this entryPoint." json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
Metrics *bool `description:"Enables metrics for this entryPoint." json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
Tracing *bool `description:"Enables tracing for this entryPoint." json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
TraceVerbosity types.TracingVerbosity `description:"Defines the tracing verbosity level for this entryPoint." json:"traceVerbosity,omitempty" toml:"traceVerbosity,omitempty" yaml:"traceVerbosity,omitempty" export:"true"`
}
// SetDefaults sets the default values.
func (o *ObservabilityConfig) SetDefaults() {
defaultValue := true
o.AccessLogs = &defaultValue
o.Tracing = &defaultValue
o.Metrics = &defaultValue
o.Tracing = &defaultValue
o.TraceVerbosity = types.MinimalVerbosity
}

View File

@ -21,6 +21,7 @@ import (
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/contrib/bridges/otellogrus"
@ -69,11 +70,16 @@ type Handler struct {
wg sync.WaitGroup
}
// WrapHandler Wraps access log handler into an Alice Constructor.
func WrapHandler(handler *Handler) alice.Constructor {
// AliceConstructor returns an alice.Constructor that wraps the Handler (conditionally) in a middleware chain.
func (h *Handler) AliceConstructor() alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
handler.ServeHTTP(rw, req, next)
if h == nil {
next.ServeHTTP(rw, req)
return
}
h.ServeHTTP(rw, req, next)
}), nil
}
}
@ -196,6 +202,12 @@ func GetLogData(req *http.Request) *LogData {
}
func (h *Handler) ServeHTTP(rw http.ResponseWriter, req *http.Request, next http.Handler) {
if !observability.AccessLogsEnabled(req.Context()) {
next.ServeHTTP(rw, req)
return
}
now := time.Now().UTC()
core := CoreLogData{

View File

@ -7,6 +7,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCommonLogFormatter_Format(t *testing.T) {
@ -82,8 +83,9 @@ func TestCommonLogFormatter_Format(t *testing.T) {
},
}
// Set timezone to Etc/GMT+9 to have a constant behavior
t.Setenv("TZ", "Etc/GMT+9")
var err error
time.Local, err = time.LoadLocation("Etc/GMT+9")
require.NoError(t, err)
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {

View File

@ -25,6 +25,7 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/collector/pdata/plog/plogotlp"
"go.opentelemetry.io/otel/attribute"
@ -105,7 +106,15 @@ func TestOTelAccessLog(t *testing.T) {
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logHandler))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logHandler.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -138,7 +147,15 @@ func TestLogRotation(t *testing.T) {
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logHandler))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logHandler.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -290,7 +307,15 @@ func TestLoggerHeaderFields(t *testing.T) {
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
}))
@ -998,7 +1023,15 @@ func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS, tracing b
chain := alice.New()
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
handler, err := chain.Then(http.HandlerFunc(logWriterTestHandlerFunc))
require.NoError(t, err)
@ -1085,7 +1118,15 @@ func doLoggingWithAbortedStream(t *testing.T, config *types.AccessLog) {
}), nil
})
chain = chain.Append(capture.Wrap)
chain = chain.Append(WrapHandler(logger))
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
service := NewFieldHandler(http.HandlerFunc(streamBackend), ServiceURL, "http://stream", nil)
service = NewFieldHandler(service, ServiceAddr, "127.0.0.1", nil)

View File

@ -7,7 +7,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -39,8 +38,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.AddPrefix, name
return result, nil
}
func (a *addPrefix) GetTracingInformation() (string, string, trace.SpanKind) {
return a.name, typeName, trace.SpanKindInternal
func (a *addPrefix) GetTracingInformation() (string, string) {
return a.name, typeName
}
func (a *addPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/singleflight"
)
@ -61,8 +60,8 @@ func NewBasic(ctx context.Context, next http.Handler, authConfig dynamic.BasicAu
return ba, nil
}
func (b *basicAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return b.name, typeNameBasic, trace.SpanKindInternal
func (b *basicAuth) GetTracingInformation() (string, string) {
return b.name, typeNameBasic
}
func (b *basicAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -54,8 +53,8 @@ func NewDigest(ctx context.Context, next http.Handler, authConfig dynamic.Digest
return da, nil
}
func (d *digestAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return d.name, typeNameDigest, trace.SpanKindInternal
func (d *digestAuth) GetTracingInformation() (string, string) {
return d.name, typeNameDigest
}
func (d *digestAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -131,8 +131,8 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
return fa, nil
}
func (fa *forwardAuth) GetTracingInformation() (string, string, trace.SpanKind) {
return fa.name, typeNameForward, trace.SpanKindInternal
func (fa *forwardAuth) GetTracingInformation() (string, string) {
return fa.name, typeNameForward
}
func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
@ -180,7 +180,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
var forwardSpan trace.Span
var tracer *tracing.Tracer
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil && observability.TracingEnabled(req.Context()) {
var tracingCtx context.Context
tracingCtx, forwardSpan = tracer.Start(req.Context(), "AuthRequest", trace.WithSpanKind(trace.SpanKindClient))
defer forwardSpan.End()

View File

@ -16,6 +16,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/proxy/httputil"
"github.com/traefik/traefik/v3/pkg/testhelpers"
"github.com/traefik/traefik/v3/pkg/tracing"
@ -756,6 +757,10 @@ func TestForwardAuthTracing(t *testing.T) {
next, err := NewForward(t.Context(), next, auth, "authTest")
require.NoError(t, err)
next = observability.WithObservabilityHandler(next, observability.Observability{
TracingEnabled: true,
})
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/search?q=Opentelemetry", nil)
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "forward-test")

View File

@ -9,7 +9,6 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
oxybuffer "github.com/vulcand/oxy/v2/buffer"
"go.opentelemetry.io/otel/trace"
)
const (
@ -48,8 +47,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.Buffering, name
}, nil
}
func (b *buffer) GetTracingInformation() (string, string, trace.SpanKind) {
return b.name, typeName, trace.SpanKindInternal
func (b *buffer) GetTracingInformation() (string, string) {
return b.name, typeName
}
func (b *buffer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/vulcand/oxy/v2/cbreaker"
"go.opentelemetry.io/otel/trace"
)
const typeName = "CircuitBreaker"
@ -68,8 +67,8 @@ func New(ctx context.Context, next http.Handler, confCircuitBreaker dynamic.Circ
}, nil
}
func (c *circuitBreaker) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
func (c *circuitBreaker) GetTracingInformation() (string, string) {
return c.name, typeName
}
func (c *circuitBreaker) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -13,7 +13,6 @@ import (
"github.com/klauspost/compress/zstd"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const typeName = "Compress"
@ -181,8 +180,8 @@ func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.R
}
}
func (c *compress) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
func (c *compress) GetTracingInformation() (string, string) {
return c.name, typeName
}
func (c *compress) newGzipHandler() (http.Handler, error) {

View File

@ -15,7 +15,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/types"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
// Compile time validation that the response recorder implements http interfaces correctly.
@ -83,8 +82,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ErrorPage, servi
}, nil
}
func (c *customErrors) GetTracingInformation() (string, string, trace.SpanKind) {
return c.name, typeName, trace.SpanKindInternal
func (c *customErrors) GetTracingInformation() (string, string) {
return c.name, typeName
}
func (c *customErrors) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -6,7 +6,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const requestHeaderModifierTypeName = "RequestHeaderModifier"
@ -35,8 +34,8 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn
}
}
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, requestHeaderModifierTypeName, trace.SpanKindUnspecified
func (r *requestHeaderModifier) GetTracingInformation() (string, string) {
return r.name, requestHeaderModifierTypeName
}
func (r *requestHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -6,7 +6,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const responseHeaderModifierTypeName = "ResponseHeaderModifier"
@ -35,8 +34,8 @@ func NewResponseHeaderModifier(ctx context.Context, next http.Handler, config dy
}
}
func (r *responseHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, responseHeaderModifierTypeName, trace.SpanKindUnspecified
func (r *responseHeaderModifier) GetTracingInformation() (string, string) {
return r.name, responseHeaderModifierTypeName
}
func (r *responseHeaderModifier) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -10,7 +10,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const typeName = "RequestRedirect"
@ -52,8 +51,8 @@ func NewRequestRedirect(ctx context.Context, next http.Handler, conf dynamic.Req
}, nil
}
func (r redirect) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
func (r redirect) GetTracingInformation() (string, string) {
return r.name, typeName
}
func (r redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -8,7 +8,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -38,8 +37,8 @@ func NewURLRewrite(ctx context.Context, next http.Handler, conf dynamic.URLRewri
}
}
func (u urlRewrite) GetTracingInformation() (string, string, trace.SpanKind) {
return u.name, typeName, trace.SpanKindInternal
func (u urlRewrite) GetTracingInformation() (string, string) {
return u.name, typeName
}
func (u urlRewrite) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -8,7 +8,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -58,8 +57,8 @@ func New(ctx context.Context, next http.Handler, cfg dynamic.Headers, name strin
}, nil
}
func (h *headers) GetTracingInformation() (string, string, trace.SpanKind) {
return h.name, typeName, trace.SpanKindInternal
func (h *headers) GetTracingInformation() (string, string) {
return h.name, typeName
}
func (h *headers) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -13,7 +13,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"go.opentelemetry.io/otel/trace"
)
func TestNew_withoutOptions(t *testing.T) {
@ -107,11 +106,10 @@ func Test_headers_getTracingInformation(t *testing.T) {
name: "testing",
}
name, typeName, spanKind := mid.GetTracingInformation()
name, typeName := mid.GetTracingInformation()
assert.Equal(t, "testing", name)
assert.Equal(t, "Headers", typeName)
assert.Equal(t, trace.SpanKindInternal, spanKind)
}
// This test is an adapted version of net/http/httputil.Test1xxResponses test.

View File

@ -10,7 +10,6 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/vulcand/oxy/v2/connlimit"
"go.opentelemetry.io/otel/trace"
)
const (
@ -53,8 +52,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.InFlightReq, nam
return &inFlightReq{handler: handler, name: name}, nil
}
func (i *inFlightReq) GetTracingInformation() (string, string, trace.SpanKind) {
return i.name, typeName, trace.SpanKindInternal
func (i *inFlightReq) GetTracingInformation() (string, string) {
return i.name, typeName
}
func (i *inFlightReq) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -11,7 +11,6 @@ import (
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -65,8 +64,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPAllowList, nam
}, nil
}
func (al *ipAllowLister) GetTracingInformation() (string, string, trace.SpanKind) {
return al.name, typeName, trace.SpanKindInternal
func (al *ipAllowLister) GetTracingInformation() (string, string) {
return al.name, typeName
}
func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -11,7 +11,6 @@ import (
"github.com/traefik/traefik/v3/pkg/ip"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -55,8 +54,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPWhiteList, nam
}, nil
}
func (wl *ipWhiteLister) GetTracingInformation() (string, string, trace.SpanKind) {
return wl.name, typeName, trace.SpanKindInternal
func (wl *ipWhiteLister) GetTracingInformation() (string, string) {
return wl.name, typeName
}
func (wl *ipWhiteLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -18,7 +18,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/retry"
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/codes"
)
@ -93,33 +92,45 @@ func NewServiceMiddleware(ctx context.Context, next http.Handler, registry metri
}
}
// WrapEntryPointHandler Wraps metrics entrypoint to alice.Constructor.
func WrapEntryPointHandler(ctx context.Context, registry metrics.Registry, entryPointName string) alice.Constructor {
// EntryPointMetricsHandler returns the metrics entrypoint handler.
func EntryPointMetricsHandler(ctx context.Context, registry metrics.Registry, entryPointName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if registry == nil || !registry.IsEpEnabled() {
return next, nil
}
return NewEntryPointMiddleware(ctx, next, registry, entryPointName), nil
}
}
// WrapRouterHandler Wraps metrics router to alice.Constructor.
func WrapRouterHandler(ctx context.Context, registry metrics.Registry, routerName string, serviceName string) alice.Constructor {
// RouterMetricsHandler returns the metrics router handler.
func RouterMetricsHandler(ctx context.Context, registry metrics.Registry, routerName string, serviceName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if registry == nil || !registry.IsRouterEnabled() {
return next, nil
}
return NewRouterMiddleware(ctx, next, registry, routerName, serviceName), nil
}
}
// WrapServiceHandler Wraps metrics service to alice.Constructor.
func WrapServiceHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor {
// ServiceMetricsHandler returns the metrics service handler.
func ServiceMetricsHandler(ctx context.Context, registry metrics.Registry, serviceName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if registry == nil || !registry.IsSvcEnabled() {
return next, nil
}
return NewServiceMiddleware(ctx, next, registry, serviceName), nil
}
}
func (m *metricsMiddleware) GetTracingInformation() (string, string, trace.SpanKind) {
return m.name, typeName, trace.SpanKindInternal
func (m *metricsMiddleware) GetTracingInformation() (string, string) {
return m.name, typeName
}
func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if val := req.Context().Value(observability.DisableMetricsKey); val != nil {
if !observability.MetricsEnabled(req.Context()) {
m.next.ServeHTTP(rw, req)
return
}

View File

@ -48,11 +48,17 @@ func newEntryPoint(ctx context.Context, tracer *tracing.Tracer, entryPointName s
}
func (e *entryPointTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if e.tracer == nil || !TracingEnabled(req.Context()) {
e.next.ServeHTTP(rw, req)
return
}
tracingCtx := tracing.ExtractCarrierIntoContext(req.Context(), req.Header)
start := time.Now()
tracingCtx, span := e.tracer.Start(tracingCtx, "EntryPoint", trace.WithSpanKind(trace.SpanKindServer), trace.WithTimestamp(start))
// Associate the request context with the logger.
// This allows the logger to be aware of the tracing context and log accordingly (TraceID, SpanID, etc.).
logger := log.Ctx(tracingCtx).With().Ctx(tracingCtx).Logger()
loggerCtx := logger.WithContext(tracingCtx)

View File

@ -14,7 +14,7 @@ import (
// Traceable embeds tracing information.
type Traceable interface {
GetTracingInformation() (name string, typeName string, spanKind trace.SpanKind)
GetTracingInformation() (name string, typeName string)
}
// WrapMiddleware adds traceability to an alice.Constructor.
@ -29,21 +29,20 @@ func WrapMiddleware(ctx context.Context, constructor alice.Constructor) alice.Co
}
if traceableHandler, ok := handler.(Traceable); ok {
name, typeName, spanKind := traceableHandler.GetTracingInformation()
name, typeName := traceableHandler.GetTracingInformation()
log.Ctx(ctx).Debug().Str(logs.MiddlewareName, name).Msg("Adding tracing to middleware")
return NewMiddleware(handler, name, typeName, spanKind), nil
return NewMiddleware(handler, name, typeName), nil
}
return handler, nil
}
}
// NewMiddleware returns a http.Handler struct.
func NewMiddleware(next http.Handler, name string, typeName string, spanKind trace.SpanKind) http.Handler {
func NewMiddleware(next http.Handler, name string, typeName string) http.Handler {
return &middlewareTracing{
next: next,
name: name,
typeName: typeName,
spanKind: spanKind,
}
}
@ -52,12 +51,11 @@ type middlewareTracing struct {
next http.Handler
name string
typeName string
spanKind trace.SpanKind
}
func (w *middlewareTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
tracingCtx, span := tracer.Start(req.Context(), w.typeName, trace.WithSpanKind(w.spanKind))
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) {
tracingCtx, span := tracer.Start(req.Context(), w.typeName, trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
req = req.WithContext(tracingCtx)

View File

@ -3,6 +3,7 @@ package observability
import (
"context"
"fmt"
"net/http"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
@ -10,8 +11,58 @@ import (
type contextKey int
// DisableMetricsKey is a context key used to disable the metrics.
const DisableMetricsKey contextKey = iota
const observabilityKey contextKey = iota
type Observability struct {
AccessLogsEnabled bool
MetricsEnabled bool
SemConvMetricsEnabled bool
TracingEnabled bool
DetailedTracingEnabled bool
}
// WithObservabilityHandler sets the observability state in the context for the next handler.
// This is also used for testing purposes to control whether access logs are enabled or not.
func WithObservabilityHandler(next http.Handler, obs Observability) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
next.ServeHTTP(rw, req.WithContext(WithObservability(req.Context(), obs)))
})
}
// WithObservability injects the observability state into the context.
func WithObservability(ctx context.Context, obs Observability) context.Context {
return context.WithValue(ctx, observabilityKey, obs)
}
// AccessLogsEnabled returns whether access-logs are enabled.
func AccessLogsEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.AccessLogsEnabled
}
// MetricsEnabled returns whether metrics are enabled.
func MetricsEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.MetricsEnabled
}
// SemConvMetricsEnabled returns whether metrics are enabled.
func SemConvMetricsEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.SemConvMetricsEnabled
}
// TracingEnabled returns whether tracing is enabled.
func TracingEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.TracingEnabled
}
// DetailedTracingEnabled returns whether detailed tracing is enabled.
func DetailedTracingEnabled(ctx context.Context) bool {
obs, ok := ctx.Value(observabilityKey).(Observability)
return ok && obs.DetailedTracingEnabled
}
// SetStatusErrorf flags the span as in error and log an event.
func SetStatusErrorf(ctx context.Context, format string, args ...interface{}) {

View File

@ -45,7 +45,7 @@ func newRouter(ctx context.Context, router, routerRule, service string, next htt
}
func (f *routerTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) {
tracingCtx, span := tracer.Start(req.Context(), "Router", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()

View File

@ -46,7 +46,7 @@ func newServerMetricsSemConv(ctx context.Context, semConvMetricRegistry *metrics
}
func (e *semConvServerMetrics) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if e.semConvMetricRegistry == nil || e.semConvMetricRegistry.HTTPServerRequestDuration() == nil {
if e.semConvMetricRegistry == nil || e.semConvMetricRegistry.HTTPServerRequestDuration() == nil || !SemConvMetricsEnabled(req.Context()) {
e.next.ServeHTTP(rw, req)
return
}

View File

@ -83,6 +83,11 @@ func TestSemConvServerMetrics(t *testing.T) {
handler, err = capture.Wrap(handler)
require.NoError(t, err)
// Injection of the observability variables in the request context.
handler = WithObservabilityHandler(handler, Observability{
SemConvMetricsEnabled: true,
})
handler.ServeHTTP(rw, req)
got := metricdata.ResourceMetrics{}

View File

@ -32,7 +32,7 @@ func NewService(ctx context.Context, service string, next http.Handler) http.Han
}
func (t *serviceTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer := tracing.TracerFromContext(req.Context()); tracer != nil && DetailedTracingEnabled(req.Context()) {
tracingCtx, span := tracer.Start(req.Context(), "Service", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()

View File

@ -14,7 +14,6 @@ import (
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const typeName = "PassClientTLSCert"
@ -139,8 +138,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.PassTLSClientCer
}, nil
}
func (p *passTLSClientCert) GetTracingInformation() (string, string, trace.SpanKind) {
return p.name, typeName, trace.SpanKindInternal
func (p *passTLSClientCert) GetTracingInformation() (string, string) {
return p.name, typeName
}
func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -14,7 +14,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
"golang.org/x/time/rate"
)
@ -127,8 +126,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.RateLimit, name
}, nil
}
func (rl *rateLimiter) GetTracingInformation() (string, string, trace.SpanKind) {
return rl.name, typeName, trace.SpanKindInternal
func (rl *rateLimiter) GetTracingInformation() (string, string) {
return rl.name, typeName
}
func (rl *rateLimiter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -6,7 +6,6 @@ import (
"regexp"
"github.com/vulcand/oxy/v2/utils"
"go.opentelemetry.io/otel/trace"
)
const (
@ -46,8 +45,8 @@ func newRedirect(next http.Handler, regex, replacement string, permanent bool, r
}, nil
}
func (r *redirect) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
func (r *redirect) GetTracingInformation() (string, string) {
return r.name, typeName
}
func (r *redirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -8,7 +8,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"go.opentelemetry.io/otel/trace"
)
const (
@ -35,8 +34,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ReplacePath, nam
}, nil
}
func (r *replacePath) GetTracingInformation() (string, string, trace.SpanKind) {
return r.name, typeName, trace.SpanKindInternal
func (r *replacePath) GetTracingInformation() (string, string) {
return r.name, typeName
}
func (r *replacePath) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -12,7 +12,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/replacepath"
"go.opentelemetry.io/otel/trace"
)
const typeName = "ReplacePathRegex"
@ -42,8 +41,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.ReplacePathRegex
}, nil
}
func (rp *replacePathRegex) GetTracingInformation() (string, string, trace.SpanKind) {
return rp.name, typeName, trace.SpanKindInternal
func (rp *replacePathRegex) GetTracingInformation() (string, string) {
return rp.name, typeName
}
func (rp *replacePathRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -14,6 +14,7 @@ import (
"github.com/cenkalti/backoff/v4"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
@ -124,7 +125,7 @@ func (r *retry) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
var currentSpan trace.Span
operation := func() error {
if tracer != nil {
if tracer != nil && observability.DetailedTracingEnabled(req.Context()) {
if currentSpan != nil {
currentSpan.End()
}

View File

@ -7,7 +7,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"go.opentelemetry.io/otel/trace"
)
const (
@ -45,8 +44,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.StripPrefix, nam
}, nil
}
func (s *stripPrefix) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindUnspecified
func (s *stripPrefix) GetTracingInformation() (string, string) {
return s.name, typeName
}
func (s *stripPrefix) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -9,7 +9,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/stripprefix"
"go.opentelemetry.io/otel/trace"
)
const (
@ -43,8 +42,8 @@ func New(ctx context.Context, next http.Handler, config dynamic.StripPrefixRegex
return &stripPrefix, nil
}
func (s *stripPrefixRegex) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindInternal
func (s *stripPrefixRegex) GetTracingInformation() (string, string) {
return s.name, typeName
}
func (s *stripPrefixRegex) ServeHTTP(rw http.ResponseWriter, req *http.Request) {

View File

@ -58,9 +58,10 @@ func Test_parseRouterConfig(t *testing.T) {
Options: "foobar",
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},

View File

@ -124,9 +124,10 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
Options: "foobar",
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},

View File

@ -242,9 +242,10 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
if ep.Observability != nil {
httpModel.Observability = dynamic.RouterObservabilityConfig{
AccessLogs: ep.Observability.AccessLogs,
Tracing: ep.Observability.Tracing,
Metrics: ep.Observability.Metrics,
AccessLogs: ep.Observability.AccessLogs,
Metrics: ep.Observability.Metrics,
Tracing: ep.Observability.Tracing,
TraceVerbosity: ep.Observability.TraceVerbosity,
}
}

View File

@ -38,17 +38,15 @@ func NewProxyBuilder(transportManager TransportManager, semConvMetricsRegistry *
func (r *ProxyBuilder) Update(_ map[string]*dynamic.ServersTransport) {}
// Build builds a new httputil.ReverseProxy with the given configuration.
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
func (r *ProxyBuilder) Build(cfgName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
roundTripper, err := r.transportManager.GetRoundTripper(cfgName)
if err != nil {
return nil, fmt.Errorf("getting RoundTripper: %w", err)
}
if shouldObserve {
// Wrapping the roundTripper with the Tracing roundTripper,
// to handle the reverseProxy client span creation.
roundTripper = newObservabilityRoundTripper(r.semConvMetricsRegistry, roundTripper)
}
// Wrapping the roundTripper with the Tracing roundTripper,
// to create, if necessary, the reverseProxy client span and the semConv client metric.
roundTripper = newObservabilityRoundTripper(r.semConvMetricsRegistry, roundTripper)
return buildSingleHostProxy(targetURL, passHostHeader, preservePath, flushInterval, roundTripper, r.bufferPool), nil
}

View File

@ -23,7 +23,7 @@ func TestEscapedPath(t *testing.T) {
roundTrippers: map[string]http.RoundTripper{"default": &http.Transport{}},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("default", testhelpers.MustParseURL(srv.URL), true, false, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(p.ServeHTTP))

View File

@ -35,7 +35,7 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
var span trace.Span
var tracingCtx context.Context
var tracer *tracing.Tracer
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil {
if tracer = tracing.TracerFromContext(req.Context()); tracer != nil && observability.TracingEnabled(req.Context()) {
tracingCtx, span = tracer.Start(req.Context(), "ReverseProxy", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()
@ -68,38 +68,42 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
span.End(trace.WithTimestamp(end))
}
if req.Context().Value(observability.DisableMetricsKey) == nil && t.semConvMetricRegistry != nil && t.semConvMetricRegistry.HTTPClientRequestDuration() != nil {
var attrs []attribute.KeyValue
if statusCode < 100 || statusCode >= 600 {
attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code %d", statusCode)))
} else if statusCode >= 400 {
attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(statusCode)))
}
attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
attrs = append(attrs, semconv.HTTPResponseStatusCode(statusCode))
attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
attrs = append(attrs, semconv.NetworkProtocolVersion(observability.Proto(req.Proto)))
attrs = append(attrs, semconv.ServerAddress(req.URL.Host))
_, port, err := net.SplitHostPort(req.URL.Host)
if err != nil {
switch req.URL.Scheme {
case "http":
attrs = append(attrs, semconv.ServerPort(80))
case "https":
attrs = append(attrs, semconv.ServerPort(443))
}
} else {
intPort, _ := strconv.Atoi(port)
attrs = append(attrs, semconv.ServerPort(intPort))
}
attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))
t.semConvMetricRegistry.HTTPClientRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
if !observability.SemConvMetricsEnabled(req.Context()) ||
t.semConvMetricRegistry == nil ||
t.semConvMetricRegistry.HTTPClientRequestDuration() == nil {
return response, err
}
var attrs []attribute.KeyValue
if statusCode < 100 || statusCode >= 600 {
attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code %d", statusCode)))
} else if statusCode >= 400 {
attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(statusCode)))
}
attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
attrs = append(attrs, semconv.HTTPResponseStatusCode(statusCode))
attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
attrs = append(attrs, semconv.NetworkProtocolVersion(observability.Proto(req.Proto)))
attrs = append(attrs, semconv.ServerAddress(req.URL.Host))
_, port, splitErr := net.SplitHostPort(req.URL.Host)
if splitErr != nil {
switch req.URL.Scheme {
case "http":
attrs = append(attrs, semconv.ServerPort(80))
case "https":
attrs = append(attrs, semconv.ServerPort(443))
}
} else {
intPort, _ := strconv.Atoi(port)
attrs = append(attrs, semconv.ServerPort(intPort))
}
attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))
t.semConvMetricRegistry.HTTPClientRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
return response, err
}

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/otel/attribute"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
@ -77,6 +78,11 @@ func TestObservabilityRoundTripper_metrics(t *testing.T) {
req.Header.Set("User-Agent", "rt-test")
req.Header.Set("X-Forwarded-Proto", "http")
// Injection of the observability variables in the request context.
req = req.WithContext(observability.WithObservability(req.Context(), observability.Observability{
SemConvMetricsEnabled: true,
}))
ort := newObservabilityRoundTripper(semConvMetricRegistry, mockRoundTripper{statusCode: test.statusCode})
_, err = ort.RoundTrip(req)
require.NoError(t, err)

View File

@ -301,7 +301,7 @@ func TestWebSocketRequestWithHeadersInResponseWriter(t *testing.T) {
},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), true, false, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testhelpers.MustParseURL(srv.URL)
@ -357,7 +357,7 @@ func TestWebSocketUpgradeFailed(t *testing.T) {
},
}
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("default@internal", testhelpers.MustParseURL(srv.URL), true, false, 0)
require.NoError(t, err)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path // keep the original path
@ -618,7 +618,7 @@ func createProxyWithForwarder(t *testing.T, uri string, transport http.RoundTrip
roundTrippers: map[string]http.RoundTripper{"fwd": transport},
}
p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, false, true, false, 0)
p, err := NewProxyBuilder(transportManager, nil).Build("fwd", u, true, false, 0)
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

View File

@ -45,7 +45,7 @@ func (b *SmartBuilder) Update(newConfigs map[string]*dynamic.ServersTransport) {
}
// Build builds an HTTP proxy for the given URL using the ServersTransport with the given name.
func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
func (b *SmartBuilder) Build(configName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error) {
serversTransport, err := b.transportManager.Get(configName)
if err != nil {
return nil, fmt.Errorf("getting ServersTransport: %w", err)
@ -55,7 +55,7 @@ func (b *SmartBuilder) Build(configName string, targetURL *url.URL, shouldObserv
// For the https scheme we cannot guess if the backend communication will use HTTP2,
// thus we check if HTTP/2 is disabled to use the fast proxy implementation when this is possible.
if targetURL.Scheme == "h2c" || (targetURL.Scheme == "https" && !serversTransport.DisableHTTP2) {
return b.proxyBuilder.Build(configName, targetURL, shouldObserve, passHostHeader, preservePath, flushInterval)
return b.proxyBuilder.Build(configName, targetURL, passHostHeader, preservePath, flushInterval)
}
return b.fastProxyBuilder.Build(configName, targetURL, passHostHeader, preservePath)
}

View File

@ -101,7 +101,7 @@ func TestSmartBuilder_Build(t *testing.T) {
httpProxyBuilder := httputil.NewProxyBuilder(transportManager, nil)
proxyBuilder := NewSmartBuilder(transportManager, httpProxyBuilder, test.fastProxyConfig)
proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, false, time.Second)
proxyHandler, err := proxyBuilder.Build("test", targetURL, false, false, time.Second)
require.NoError(t, err)
rw := httptest.NewRecorder()

View File

@ -10,6 +10,7 @@ import (
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/server/provider"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoints []string) dynamic.Configuration {
@ -208,6 +209,10 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cp.Observability.Tracing = m.Observability.Tracing
}
if cp.Observability.TraceVerbosity == "" {
cp.Observability.TraceVerbosity = m.Observability.TraceVerbosity
}
rtName := name
if len(eps) > 1 {
rtName = epName + "-" + name
@ -224,7 +229,7 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cfg.HTTP.Routers = rts
}
// Apply default observability model to HTTP routers.
// Apply the default observability model to HTTP routers.
applyDefaultObservabilityModel(cfg)
if cfg.TCP == nil || len(cfg.TCP.Models) == 0 {
@ -256,14 +261,16 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
// and make sure it is serialized and available in the API.
// We could have introduced a "default" model, but it would have been more complex to manage for now.
// This could be generalized in the future.
// TODO: check if we can remove this and rely on the SetDefaults instead.
func applyDefaultObservabilityModel(cfg dynamic.Configuration) {
if cfg.HTTP != nil {
for _, router := range cfg.HTTP.Routers {
if router.Observability == nil {
router.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
}
continue
@ -273,12 +280,16 @@ func applyDefaultObservabilityModel(cfg dynamic.Configuration) {
router.Observability.AccessLogs = pointer(true)
}
if router.Observability.Metrics == nil {
router.Observability.Metrics = pointer(true)
}
if router.Observability.Tracing == nil {
router.Observability.Tracing = pointer(true)
}
if router.Observability.Metrics == nil {
router.Observability.Metrics = pointer(true)
if router.Observability.TraceVerbosity == "" {
router.Observability.TraceVerbosity = types.MinimalVerbosity
}
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/tls"
"github.com/traefik/traefik/v3/pkg/types"
)
func Test_mergeConfiguration(t *testing.T) {
@ -521,9 +522,10 @@ func Test_applyModel(t *testing.T) {
Routers: map[string]*dynamic.Router{
"test": {
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -589,9 +591,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -622,9 +625,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -638,9 +642,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -651,9 +656,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -688,9 +694,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{CertResolver: "router"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},
@ -730,9 +737,10 @@ func Test_applyModel(t *testing.T) {
"test": {
EntryPoints: []string{"web"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
"websecure-test": {
@ -740,9 +748,10 @@ func Test_applyModel(t *testing.T) {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
},
},
},

View File

@ -428,8 +428,5 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
}
// The tracing middleware is a NOOP if tracing is not setup on the middleware chain.
// Hence, regarding internal resources' observability deactivation,
// this would not enable tracing.
return observability.WrapMiddleware(ctx, middleware), nil
}

View File

@ -4,7 +4,6 @@ import (
"context"
"io"
"net/http"
"strings"
"github.com/containous/alice"
"github.com/rs/zerolog/log"
@ -17,6 +16,7 @@ import (
mmetrics "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/types"
)
// ObservabilityMgr is a manager for observability (AccessLogs, Metrics and Tracing) enablement.
@ -42,111 +42,44 @@ func NewObservabilityMgr(config static.Configuration, metricsRegistry metrics.Re
}
// BuildEPChain an observability middleware chain by entry point.
func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, resourceName string, observabilityConfig *dynamic.RouterObservabilityConfig) alice.Chain {
func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, internal bool, config dynamic.RouterObservabilityConfig) alice.Chain {
chain := alice.New()
if o == nil {
return chain
}
if o.accessLoggerMiddleware != nil || o.metricsRegistry != nil && (o.metricsRegistry.IsEpEnabled() || o.metricsRegistry.IsRouterEnabled() || o.metricsRegistry.IsSvcEnabled()) {
if o.ShouldAddAccessLogs(resourceName, observabilityConfig) || o.ShouldAddMetrics(resourceName, observabilityConfig) {
chain = chain.Append(capture.Wrap)
}
// Injection of the observability variables in the request context.
// This injection must be the first step in order for other observability middlewares to rely on it.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return o.observabilityContextHandler(next, internal, config), nil
})
// Capture middleware for accessLogs or metrics.
if o.shouldAccessLog(internal, config) || o.shouldMeter(internal, config) || o.shouldMeterSemConv(internal, config) {
chain = chain.Append(capture.Wrap)
}
// As the Entry point observability middleware ensures that the tracing is added to the request and logger context,
// it needs to be added before the access log middleware to ensure that the trace ID is logged.
if o.tracer != nil && o.ShouldAddTracing(resourceName, observabilityConfig) {
chain = chain.Append(observability.EntryPointHandler(ctx, o.tracer, entryPointName))
}
chain = chain.Append(observability.EntryPointHandler(ctx, o.tracer, entryPointName))
if o.accessLoggerMiddleware != nil && o.ShouldAddAccessLogs(resourceName, observabilityConfig) {
chain = chain.Append(accesslog.WrapHandler(o.accessLoggerMiddleware))
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil
})
}
// Access log handlers.
chain = chain.Append(o.accessLoggerMiddleware.AliceConstructor())
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, logs.EntryPointName, entryPointName, accesslog.InitServiceFields), nil
})
// Entrypoint metrics handler.
metricsHandler := mmetrics.EntryPointMetricsHandler(ctx, o.metricsRegistry, entryPointName)
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
// Semantic convention server metrics handler.
if o.semConvMetricRegistry != nil && o.ShouldAddMetrics(resourceName, observabilityConfig) {
chain = chain.Append(observability.SemConvServerMetricsHandler(ctx, o.semConvMetricRegistry))
}
if o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName, observabilityConfig) {
metricsHandler := mmetrics.WrapEntryPointHandler(ctx, o.metricsRegistry, entryPointName)
if o.tracer != nil && o.ShouldAddTracing(resourceName, observabilityConfig) {
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
} else {
chain = chain.Append(metricsHandler)
}
}
// Inject context keys to control whether to produce metrics further downstream (services, round-tripper),
// because the router configuration cannot be evaluated during build time for services.
if observabilityConfig != nil && observabilityConfig.Metrics != nil && !*observabilityConfig.Metrics {
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
next.ServeHTTP(rw, req.WithContext(context.WithValue(req.Context(), observability.DisableMetricsKey, true)))
}), nil
})
}
chain = chain.Append(observability.SemConvServerMetricsHandler(ctx, o.semConvMetricRegistry))
return chain
}
// ShouldAddAccessLogs returns whether the access logs should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) ShouldAddAccessLogs(serviceName string, observabilityConfig *dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.AccessLog == nil {
return false
}
if strings.HasSuffix(serviceName, "@internal") && !o.config.AccessLog.AddInternals {
return false
}
return observabilityConfig == nil || observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
}
// ShouldAddMetrics returns whether the metrics should be enabled for the given resource and the observability config.
func (o *ObservabilityMgr) ShouldAddMetrics(serviceName string, observabilityConfig *dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.Metrics == nil {
return false
}
if strings.HasSuffix(serviceName, "@internal") && !o.config.Metrics.AddInternals {
return false
}
return observabilityConfig == nil || observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// ShouldAddTracing returns whether the tracing should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) ShouldAddTracing(serviceName string, observabilityConfig *dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.Tracing == nil {
return false
}
if strings.HasSuffix(serviceName, "@internal") && !o.config.Tracing.AddInternals {
return false
}
return observabilityConfig == nil || observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
}
// MetricsRegistry is an accessor to the metrics registry.
func (o *ObservabilityMgr) MetricsRegistry() metrics.Registry {
if o == nil {
@ -191,3 +124,89 @@ func (o *ObservabilityMgr) RotateAccessLogs() error {
return o.accessLoggerMiddleware.Rotate()
}
func (o *ObservabilityMgr) observabilityContextHandler(next http.Handler, internal bool, config dynamic.RouterObservabilityConfig) http.Handler {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: o.shouldAccessLog(internal, config),
MetricsEnabled: o.shouldMeter(internal, config),
SemConvMetricsEnabled: o.shouldMeterSemConv(internal, config),
TracingEnabled: o.shouldTrace(internal, config, types.MinimalVerbosity),
DetailedTracingEnabled: o.shouldTrace(internal, config, types.DetailedVerbosity),
})
}
// shouldAccessLog returns whether the access logs should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldAccessLog(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
if o == nil {
return false
}
if o.config.AccessLog == nil {
return false
}
if internal && !o.config.AccessLog.AddInternals {
return false
}
return observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
}
// shouldMeter returns whether the metrics should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldMeter(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
if o == nil || o.metricsRegistry == nil {
return false
}
if !o.metricsRegistry.IsEpEnabled() && !o.metricsRegistry.IsRouterEnabled() && !o.metricsRegistry.IsSvcEnabled() {
return false
}
if o.config.Metrics == nil {
return false
}
if internal && !o.config.Metrics.AddInternals {
return false
}
return observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// shouldMeterSemConv returns whether the OTel semantic convention metrics should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldMeterSemConv(internal bool, observabilityConfig dynamic.RouterObservabilityConfig) bool {
if o == nil || o.semConvMetricRegistry == nil {
return false
}
if o.config.Metrics == nil {
return false
}
if internal && !o.config.Metrics.AddInternals {
return false
}
return observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
}
// shouldTrace returns whether the tracing should be enabled for the given serviceName and the observability config.
func (o *ObservabilityMgr) shouldTrace(internal bool, observabilityConfig dynamic.RouterObservabilityConfig, verbosity types.TracingVerbosity) bool {
if o == nil {
return false
}
if o.config.Tracing == nil {
return false
}
if internal && !o.config.Tracing.AddInternals {
return false
}
if !observabilityConfig.TraceVerbosity.Allows(verbosity) {
return false
}
return observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
}

View File

@ -7,7 +7,6 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/plugins"
"go.opentelemetry.io/otel/trace"
)
const typeName = "Plugin"
@ -55,6 +54,6 @@ func (s *traceablePlugin) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
s.h.ServeHTTP(rw, req)
}
func (s *traceablePlugin) GetTracingInformation() (string, string, trace.SpanKind) {
return s.name, typeName, trace.SpanKindInternal
func (s *traceablePlugin) GetTracingInformation() (string, string) {
return s.name, typeName
}

View File

@ -10,6 +10,7 @@ import (
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/config/runtime"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
@ -70,11 +71,22 @@ func (m *Manager) getHTTPRouters(ctx context.Context, entryPoints []string, tls
func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, tls bool) map[string]http.Handler {
entryPointHandlers := make(map[string]http.Handler)
defaultObsConfig := dynamic.RouterObservabilityConfig{}
defaultObsConfig.SetDefaults()
for entryPointName, routers := range m.getHTTPRouters(rootCtx, entryPoints, tls) {
logger := log.Ctx(rootCtx).With().Str(logs.EntryPointName, entryPointName).Logger()
ctx := logger.WithContext(rootCtx)
handler, err := m.buildEntryPointHandler(ctx, entryPointName, routers)
// TODO: Improve this part. Relying on models is a shortcut to get the entrypoint observability configuration. Maybe we should pass down the static configuration.
// When the entry point has no observability configuration no model is produced,
// and we need to create the default configuration is this case.
epObsConfig := defaultObsConfig
if model, ok := m.conf.Models[entryPointName+"@internal"]; ok && model != nil {
epObsConfig = model.Observability
}
handler, err := m.buildEntryPointHandler(ctx, entryPointName, routers, epObsConfig)
if err != nil {
logger.Error().Err(err).Send()
continue
@ -93,7 +105,15 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
continue
}
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "", nil).Then(BuildDefaultHTTPRouter())
// TODO: Improve this part. Relying on models is a shortcut to get the entrypoint observability configuration. Maybe we should pass down the static configuration.
// When the entry point has no observability configuration no model is produced,
// and we need to create the default configuration is this case.
epObsConfig := defaultObsConfig
if model, ok := m.conf.Models[entryPointName+"@internal"]; ok && model != nil {
epObsConfig = model.Observability
}
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, false, epObsConfig).Then(http.NotFoundHandler())
if err != nil {
logger.Error().Err(err).Send()
continue
@ -104,10 +124,10 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
return entryPointHandlers
}
func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo) (http.Handler, error) {
func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName string, configs map[string]*runtime.RouterInfo, config dynamic.RouterObservabilityConfig) (http.Handler, error) {
muxer := httpmuxer.NewMuxer(m.parser)
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "", nil).Then(http.NotFoundHandler())
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, false, config).Then(http.NotFoundHandler())
if err != nil {
return nil, err
}
@ -136,7 +156,11 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
continue
}
observabilityChain := m.observabilityMgr.BuildEPChain(ctx, entryPointName, routerConfig.Service, routerConfig.Observability)
if routerConfig.Observability != nil {
config = *routerConfig.Observability
}
observabilityChain := m.observabilityMgr.BuildEPChain(ctxRouter, entryPointName, strings.HasSuffix(routerConfig.Service, "@internal"), config)
handler, err = observabilityChain.Then(handler)
if err != nil {
routerConfig.AddError(err, true)
@ -180,22 +204,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, rou
return nil, err
}
// Prevents from enabling observability for internal resources.
if !m.observabilityMgr.ShouldAddAccessLogs(provider.GetQualifiedName(ctx, routerConfig.Service), routerConfig.Observability) {
m.routerHandlers[routerName] = handler
return m.routerHandlers[routerName], nil
}
handlerWithAccessLog, err := alice.New(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
}).Then(handler)
if err != nil {
log.Ctx(ctx).Error().Err(err).Send()
m.routerHandlers[routerName] = handler
} else {
m.routerHandlers[routerName] = handlerWithAccessLog
}
m.routerHandlers[routerName] = handler
return m.routerHandlers[routerName], nil
}
@ -210,40 +219,29 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
return nil, errors.New("the service is missing on the router")
}
sHandler, err := m.serviceManager.BuildHTTP(ctx, router.Service)
if err != nil {
return nil, err
}
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
qualifiedService := provider.GetQualifiedName(ctx, router.Service)
chain := alice.New()
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() &&
m.observabilityMgr.ShouldAddMetrics(provider.GetQualifiedName(ctx, router.Service), router.Observability) {
chain = chain.Append(metricsMiddle.WrapRouterHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, provider.GetQualifiedName(ctx, router.Service)))
}
// Prevents from enabling tracing for internal resources.
if !m.observabilityMgr.ShouldAddTracing(provider.GetQualifiedName(ctx, router.Service), router.Observability) {
return chain.Extend(*mHandler).Then(sHandler)
}
chain = chain.Append(observability.WrapRouterHandler(ctx, routerName, router.Rule, provider.GetQualifiedName(ctx, router.Service)))
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() {
metricsHandler := metricsMiddle.WrapRouterHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, provider.GetQualifiedName(ctx, router.Service))
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
}
if router.DefaultRule {
chain = chain.Append(denyrouterrecursion.WrapHandler(routerName))
}
// Access logs, metrics, and tracing middlewares are idempotent if the associated signal is disabled.
chain = chain.Append(observability.WrapRouterHandler(ctx, routerName, router.Rule, qualifiedService))
metricsHandler := metricsMiddle.RouterMetricsHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, qualifiedService)
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
})
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
sHandler, err := m.serviceManager.BuildHTTP(ctx, qualifiedService)
if err != nil {
return nil, err
}
return chain.Extend(*mHandler).Then(sHandler)
}
// BuildDefaultHTTPRouter creates a default HTTP router.
func BuildDefaultHTTPRouter() http.Handler {
return http.NotFoundHandler()
}

View File

@ -929,7 +929,7 @@ func BenchmarkService(b *testing.B) {
type proxyBuilderMock struct{}
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _, _ bool, _ time.Duration) (http.Handler, error) {
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}

View File

@ -258,7 +258,7 @@ func TestInternalServices(t *testing.T) {
type proxyBuilderMock struct{}
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _, _ bool, _ time.Duration) (http.Handler, error) {
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
return http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {}), nil
}

View File

@ -29,7 +29,6 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/forwardedheaders"
"github.com/traefik/traefik/v3/pkg/middlewares/requestdecorator"
"github.com/traefik/traefik/v3/pkg/safe"
"github.com/traefik/traefik/v3/pkg/server/router"
tcprouter "github.com/traefik/traefik/v3/pkg/server/router/tcp"
"github.com/traefik/traefik/v3/pkg/server/service"
"github.com/traefik/traefik/v3/pkg/tcp"
@ -351,7 +350,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
httpHandler := rt.GetHTTPHandler()
if httpHandler == nil {
httpHandler = router.BuildDefaultHTTPRouter()
httpHandler = http.NotFoundHandler()
}
e.httpServer.Switcher.UpdateHandler(httpHandler)
@ -360,7 +359,7 @@ func (e *TCPEntryPoint) SwitchRouter(rt *tcprouter.Router) {
httpsHandler := rt.GetHTTPSHandler()
if httpsHandler == nil {
httpsHandler = router.BuildDefaultHTTPRouter()
httpsHandler = http.NotFoundHandler()
}
e.httpsServer.Switcher.UpdateHandler(httpsHandler)
@ -591,7 +590,7 @@ func createHTTPServer(ctx context.Context, ln net.Listener, configuration *stati
return nil, errors.New("max concurrent streams value must be greater than or equal to zero")
}
httpSwitcher := middlewares.NewHandlerSwitcher(router.BuildDefaultHTTPRouter())
httpSwitcher := middlewares.NewHandlerSwitcher(http.NotFoundHandler())
next, err := alice.New(requestdecorator.WrapHandler(reqDecorator)).Then(httpSwitcher)
if err != nil {

View File

@ -19,7 +19,6 @@ import (
"github.com/traefik/traefik/v3/pkg/healthcheck"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
metricsMiddle "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/middlewares/retry"
@ -37,7 +36,7 @@ import (
// ProxyBuilder builds reverse proxy handlers.
type ProxyBuilder interface {
Build(cfgName string, targetURL *url.URL, shouldObserve, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error)
Build(cfgName string, targetURL *url.URL, passHostHeader, preservePath bool, flushInterval time.Duration) (http.Handler, error)
Update(configs map[string]*dynamic.ServersTransport)
}
@ -364,50 +363,32 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName, nil) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil)
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, server.PreservePath, flushInterval)
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, passHostHeader, server.PreservePath, flushInterval)
if err != nil {
return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err)
}
// The retry wrapping must be done just before the proxy handler,
// to make sure that the retry will not be triggered/disabled by
// middlewares in the chain.
proxy = retry.WrapHandler(proxy)
// Prevents from enabling observability for internal resources.
// Access logs, metrics, and tracing middlewares are idempotent if the associated signal is disabled.
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, qualifiedSvcName, accesslog.AddServiceFields)
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName, nil) {
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceURL, target.String(), nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceAddr, target.Host, nil)
proxy = accesslog.NewFieldHandler(proxy, accesslog.ServiceName, serviceName, accesslog.AddServiceFields)
metricsHandler := metricsMiddle.ServiceMetricsHandler(ctx, m.observabilityMgr.MetricsRegistry(), qualifiedSvcName)
metricsHandler = observability.WrapMiddleware(ctx, metricsHandler)
proxy, err = alice.New().
Append(metricsHandler).
Then(proxy)
if err != nil {
return nil, fmt.Errorf("error wrapping metrics handler: %w", err)
}
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsSvcEnabled() &&
m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil) {
metricsHandler := metricsMiddle.WrapServiceHandler(ctx, m.observabilityMgr.MetricsRegistry(), serviceName)
proxy, err = alice.New().
Append(observability.WrapMiddleware(ctx, metricsHandler)).
Then(proxy)
if err != nil {
return nil, fmt.Errorf("error wrapping metrics handler: %w", err)
}
}
if m.observabilityMgr.ShouldAddTracing(qualifiedSvcName, nil) {
proxy = observability.NewService(ctx, serviceName, proxy)
}
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName, nil) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil) {
// Some piece of middleware, like the ErrorPage, are relying on this serviceBuilder to get the handler for a given service,
// to re-target the request to it.
// Those pieces of middleware can be configured on routes that expose a Traefik internal service.
// In such a case, observability for internals being optional, the capture probe could be absent from context (no wrap via the entrypoint).
// But if the service targeted by this piece of middleware is not an internal one,
// and requires observability, we still want the capture probe to be present in the request context.
// Makes sure a capture probe is in the request context.
proxy, _ = capture.Wrap(proxy)
}
proxy = observability.NewService(ctx, qualifiedSvcName, proxy)
lb.AddServer(server.URL, proxy, server)

View File

@ -2,6 +2,7 @@ package testhelpers
import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/types"
)
// BuildConfiguration is a helper to create a configuration.
@ -57,9 +58,10 @@ func WithServiceName(serviceName string) func(*dynamic.Router) {
func WithObservability() func(*dynamic.Router) {
return func(r *dynamic.Router) {
r.Observability = &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
AccessLogs: pointer(true),
Metrics: pointer(true),
Tracing: pointer(true),
TraceVerbosity: types.MinimalVerbosity,
}
}
}

View File

@ -23,6 +23,22 @@ import (
"google.golang.org/grpc/encoding/gzip"
)
type TracingVerbosity string
const (
MinimalVerbosity TracingVerbosity = "minimal"
DetailedVerbosity TracingVerbosity = "detailed"
)
func (v TracingVerbosity) Allows(verbosity TracingVerbosity) bool {
switch v {
case DetailedVerbosity:
return verbosity == DetailedVerbosity || verbosity == MinimalVerbosity
default:
return verbosity == MinimalVerbosity
}
}
// OTelTracing provides configuration settings for the open-telemetry tracer.
type OTelTracing struct {
GRPC *OTelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`

72
pkg/types/tracing_test.go Normal file
View File

@ -0,0 +1,72 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestTracingVerbosity_Allows(t *testing.T) {
tests := []struct {
desc string
from TracingVerbosity
to TracingVerbosity
allows bool
}{
{
desc: "minimal vs minimal",
from: MinimalVerbosity,
to: MinimalVerbosity,
allows: true,
},
{
desc: "minimal vs detailed",
from: MinimalVerbosity,
to: DetailedVerbosity,
allows: false,
},
{
desc: "detailed vs minimal",
from: DetailedVerbosity,
to: MinimalVerbosity,
allows: true,
},
{
desc: "detailed vs detailed",
from: DetailedVerbosity,
to: DetailedVerbosity,
allows: true,
},
{
desc: "unknown vs minimal",
from: TracingVerbosity("unknown"),
to: MinimalVerbosity,
allows: true,
},
{
desc: "unknown vs detailed",
from: TracingVerbosity("unknown"),
to: DetailedVerbosity,
allows: false,
},
{
desc: "minimal vs unknown",
from: MinimalVerbosity,
to: TracingVerbosity("unknown"),
allows: false,
},
{
desc: "detailed vs unknown",
from: DetailedVerbosity,
to: TracingVerbosity("unknown"),
allows: false,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.allows, test.from.Allows(test.to))
})
}
}