From f2b7d7f6e10fe32fae66a8e33818273ee363fd80 Mon Sep 17 00:00:00 2001 From: Adrien Kunysz Date: Thu, 17 Jul 2025 15:28:05 +0200 Subject: [PATCH 01/18] Fix typo --- docs/content/reference/install-configuration/entrypoints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/reference/install-configuration/entrypoints.md b/docs/content/reference/install-configuration/entrypoints.md index 9c0aab31c..e3bb8ad40 100644 --- a/docs/content/reference/install-configuration/entrypoints.md +++ b/docs/content/reference/install-configuration/entrypoints.md @@ -213,7 +213,7 @@ only routers with TLS enabled will be usable with HTTP/3. ### ProxyProtocol and Load-Balancers -The replacement of the remote client address will occur only for IP addresses listed in `trustedIPs`. This is where yoåu specify your load balancer IPs or CIDR ranges. +The replacement of the remote client address will occur only for IP addresses listed in `trustedIPs`. This is where you specify your load balancer IPs or CIDR ranges. When queuing Traefik behind another load-balancer, make sure to configure PROXY protocol on both sides. From 8c23eb683339d8072eb0e4786e24a3ed221d18a6 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 18 Jul 2025 15:32:05 +0200 Subject: [PATCH 02/18] Introduce trace verbosity config and produce less spans by default --- docs/content/migration/v3.md | 20 ++ .../dynamic-configuration/docker-labels.yml | 2 + .../reference/dynamic-configuration/file.toml | 6 +- .../reference/dynamic-configuration/file.yaml | 6 +- .../kubernetes-crd-definition-v1.yml | 11 + .../reference/dynamic-configuration/kv-ref.md | 2 + .../traefik.io_ingressroutes.yaml | 11 + .../install-configuration/entrypoints.md | 77 ++++--- .../http/router/observability.md | 26 ++- .../reference/static-configuration/cli-ref.md | 9 +- .../reference/static-configuration/env-ref.md | 9 +- .../reference/static-configuration/file.toml | 3 +- .../reference/static-configuration/file.yaml | 3 +- integration/access_log_test.go | 19 -- integration/fixtures/k8s/01-traefik-crd.yml | 11 + .../tracing/simple-opentelemetry.toml | 10 +- integration/resources/compose/access_log.yml | 10 - integration/testdata/rawdata-consul.json | 21 +- .../testdata/rawdata-crd-label-selector.json | 6 +- integration/testdata/rawdata-crd.json | 18 +- integration/testdata/rawdata-etcd.json | 21 +- integration/testdata/rawdata-gateway.json | 12 +- .../rawdata-ingress-label-selector.json | 9 +- integration/testdata/rawdata-ingress.json | 18 +- .../rawdata-ingressclass-disabled.json | 6 +- .../testdata/rawdata-ingressclass.json | 9 +- integration/testdata/rawdata-redis.json | 22 +- integration/testdata/rawdata-zk.json | 21 +- integration/tracing_test.go | 114 +++++++++- pkg/config/dynamic/http_config.go | 16 +- pkg/config/dynamic/zz_generated.deepcopy.go | 8 +- pkg/config/runtime/runtime.go | 5 +- pkg/config/static/entrypoints.go | 10 +- pkg/middlewares/accesslog/logger.go | 18 +- .../accesslog/logger_formatters_test.go | 6 +- pkg/middlewares/accesslog/logger_test.go | 51 ++++- pkg/middlewares/addprefix/add_prefix.go | 5 +- pkg/middlewares/auth/basic_auth.go | 5 +- pkg/middlewares/auth/digest_auth.go | 5 +- pkg/middlewares/auth/forward.go | 6 +- pkg/middlewares/auth/forward_test.go | 5 + pkg/middlewares/buffering/buffering.go | 5 +- .../circuitbreaker/circuit_breaker.go | 5 +- pkg/middlewares/compress/compress.go | 5 +- pkg/middlewares/customerrors/custom_errors.go | 5 +- .../headermodifier/request_header_modifier.go | 5 +- .../response_header_modifier.go | 5 +- .../gatewayapi/redirect/request_redirect.go | 5 +- .../gatewayapi/urlrewrite/url_rewrite.go | 5 +- pkg/middlewares/headers/headers.go | 5 +- pkg/middlewares/headers/headers_test.go | 4 +- pkg/middlewares/inflightreq/inflight_req.go | 5 +- pkg/middlewares/ipallowlist/ip_allowlist.go | 5 +- pkg/middlewares/ipwhitelist/ip_whitelist.go | 5 +- pkg/middlewares/metrics/metrics.go | 31 ++- pkg/middlewares/observability/entrypoint.go | 6 + pkg/middlewares/observability/middleware.go | 14 +- .../observability/observability.go | 55 ++++- pkg/middlewares/observability/router.go | 2 +- pkg/middlewares/observability/semconv.go | 2 +- pkg/middlewares/observability/semconv_test.go | 5 + pkg/middlewares/observability/service.go | 2 +- .../passtlsclientcert/pass_tls_client_cert.go | 5 +- pkg/middlewares/ratelimiter/rate_limiter.go | 5 +- pkg/middlewares/redirect/redirect.go | 5 +- pkg/middlewares/replacepath/replace_path.go | 5 +- .../replacepathregex/replace_path_regex.go | 5 +- pkg/middlewares/retry/retry.go | 3 +- pkg/middlewares/stripprefix/strip_prefix.go | 5 +- .../stripprefixregex/strip_prefix_regex.go | 5 +- .../kubernetes/ingress/annotations_test.go | 7 +- .../kubernetes/ingress/kubernetes_test.go | 7 +- pkg/provider/traefik/internal.go | 7 +- pkg/proxy/httputil/builder.go | 10 +- pkg/proxy/httputil/builder_test.go | 2 +- pkg/proxy/httputil/observability.go | 68 +++--- pkg/proxy/httputil/observability_test.go | 6 + pkg/proxy/httputil/proxy_websocket_test.go | 6 +- pkg/proxy/smart_builder.go | 4 +- pkg/proxy/smart_builder_test.go | 2 +- pkg/server/aggregator.go | 23 +- pkg/server/aggregator_test.go | 57 ++--- pkg/server/middleware/middlewares.go | 3 - pkg/server/middleware/observability.go | 197 ++++++++++-------- pkg/server/middleware/plugins.go | 5 +- pkg/server/router/router.go | 96 +++++---- pkg/server/router/router_test.go | 2 +- pkg/server/routerfactory_test.go | 2 +- pkg/server/server_entrypoint_tcp.go | 7 +- pkg/server/service/service.go | 51 ++--- pkg/testhelpers/config.go | 8 +- pkg/types/tracing.go | 16 ++ pkg/types/tracing_test.go | 72 +++++++ 93 files changed, 1005 insertions(+), 524 deletions(-) create mode 100644 pkg/types/tracing_test.go diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md index 6581b5f39..e3cbe8a9b 100644 --- a/docs/content/migration/v3.md +++ b/docs/content/migration/v3.md @@ -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). diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index b2af3def6..3399c2762 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -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" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 202815c39..b7f30c649 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -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] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index e8210ef85..2ac73fd85 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -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: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index d3f71b5f9..22a08a7d7 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -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: diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 733b1bdb5..a7c1b217f 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -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` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 0a5d6749a..46cc78976 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -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: diff --git a/docs/content/reference/install-configuration/entrypoints.md b/docs/content/reference/install-configuration/entrypoints.md index 9c0aab31c..2ae13200e 100644 --- a/docs/content/reference/install-configuration/entrypoints.md +++ b/docs/content/reference/install-configuration/entrypoints.md @@ -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.
It also defines the protocol to use (TCP or UDP).
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`.
`entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.
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-*`).
We recommend to use this option only for tests purposes, not in production. | false | No | -| `http.redirections.`
`entryPoint.to` | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one.
The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes | -| `http.redirections.`
`entryPoint.scheme` | The target scheme to use for (permanent) redirection of all incoming requests. | https | No | -| `http.redirections.`
`entryPoint.permanent` | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme.
The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No | -| `http.redirections.`
`entryPoint.priority` | Default priority applied to the routers attached to the `entryPoint`.| MaxInt32-1 (2147483646) | No | -| `http.encodeQuerySemicolons` | Enable query semicolons encoding.
Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik.
When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.
More information [here](#encodequerysemicolons). | false | No | -| `http.sanitizePath` | Defines whether to enable the request path sanitization.
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.
More information [here](#httpmiddlewares). | - | No | -| `http.tls` | Enable TLS on every router attached to the `entryPoint`.
If no certificate are set, a default self-signed certificate is generates by Traefik.
We recommend to not use self signed certificates in production.| - | No | -| `http.tls.options` | Apply TLS options on every router attached to the `entryPoint`.
The TLS options can be overidden per router.
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`.
The TLS options can be overidden per router.
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.
The value must be greater than zero. | 250 | No | -| `http3` | Enable HTTP/3 protocol on the `entryPoint`.
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.
More information [here](#http3. | - | No | -| `http3.advertisedPort` | Set the UDP port to advertise as the HTTP/3 authority.
It defaults to the entryPoint's address port.
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.
Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2.
If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers.
If the PROXY protocol header is passed, then the version is determined automatically.
More information [here](#proxyprotocol-and-load-balancers). | - | No | -| `proxyProtocol.insecure` | Enable PROXY protocol trusting every incoming connection.
Every remote client address will be replaced (`trustedIPs`) won't have any effect).
Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2.
If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers.
If the PROXY protocol header is passed, then the version is determined automatically.
We recommend to use this option only for tests purposes, not in production.
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.
It also allows the kernel to act like a load balancer to distribute incoming connections between entry points..
More information [here](#reuseport). | false | No | -| `transport.`
`respondingTimeouts.`
`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`.
If zero, no timeout exists.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds. | 60s (seconds) | No | -| `transport.`
`respondingTimeouts.`
`writeTimeout` | Maximum duration before timing out writes of the response.
It covers the time from the end of the request header read to the end of the response write.
If zero, no timeout exists.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds. | 0s (seconds) | No | -| `transport.`
`respondingTimeouts.`
`idleTimeout` | Maximum duration an idle (keep-alive) connection will remain idle before closing itself.
If zero, no timeout exists
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds | 180s (seconds) | No | -| `transport.`
`lifeCycle.`
`graceTimeOut` | Set the duration to give active requests a chance to finish before Traefik stops.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds
In this time frame no new requests are accepted. | 10s (seconds) | No | -| `transport.`
`lifeCycle.`
`requestAcceptGraceTimeout` | Set the duration to keep accepting requests prior to initiating the graceful termination period (as defined by the `transportlifeCycle.graceTimeOut` option).
This option is meant to give downstream load-balancers sufficient time to take Traefik out of rotation.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds | 0s (seconds) | No | -| `transport.`
`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).
Zero means no limit. | 0 | No | -| `transport.`
`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.
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.
It also defines the protocol to use (TCP or UDP).
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`.
`entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.
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-*`).
We recommend to use this option only for tests purposes, not in production. | false | No | +| `http.redirections.`
`entryPoint.to` | The target element to enable (permanent) redirecting of all incoming requests on an entry point to another one.
The target element can be an entry point name (ex: `websecure`), or a port (`:443`). | - | Yes | +| `http.redirections.`
`entryPoint.scheme` | The target scheme to use for (permanent) redirection of all incoming requests. | https | No | +| `http.redirections.`
`entryPoint.permanent` | Enable permanent redirecting of all incoming requests on an entry point to another one changing the scheme.
The target element, it can be an entry point name (ex: `websecure`), or a port (`:443`). | false | No | +| `http.redirections.`
`entryPoint.priority` | Default priority applied to the routers attached to the `entryPoint`. | MaxInt32-1 (2147483646) | No | +| `http.encodeQuerySemicolons` | Enable query semicolons encoding.
Use this option to avoid non-encoded semicolons to be interpreted as query parameter separators by Traefik.
When using this option, the non-encoded semicolons characters in query will be transmitted encoded to the backend.
More information [here](#encodequerysemicolons). | false | No | +| `http.sanitizePath` | Defines whether to enable the request path sanitization.
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.
More information [here](#httpmiddlewares). | - | No | +| `http.tls` | Enable TLS on every router attached to the `entryPoint`.
If no certificate are set, a default self-signed certificate is generates by Traefik.
We recommend to not use self signed certificates in production. | - | No | +| `http.tls.options` | Apply TLS options on every router attached to the `entryPoint`.
The TLS options can be overidden per router.
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`.
The TLS options can be overidden per router.
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.
The value must be greater than zero. | 250 | No | +| `http3` | Enable HTTP/3 protocol on the `entryPoint`.
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.
More information [here](#http3. | - | No | +| `http3.advertisedPort` | Set the UDP port to advertise as the HTTP/3 authority.
It defaults to the entryPoint's address port.
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.
More information [here](#traceverbosity). | minimal | No | +| `proxyProtocol.trustedIPs` | Enable PROXY protocol with Trusted IPs.
Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2.
If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers.
If the PROXY protocol header is passed, then the version is determined automatically.
More information [here](#proxyprotocol-and-load-balancers). | - | No | +| `proxyProtocol.insecure` | Enable PROXY protocol trusting every incoming connection.
Every remote client address will be replaced (`trustedIPs`) won't have any effect).
Traefik supports [PROXY protocol](https://www.haproxy.org/download/2.0/doc/proxy-protocol.txt) version 1 and 2.
If PROXY protocol header parsing is enabled for the entry point, this entry point can accept connections with or without PROXY protocol headers.
If the PROXY protocol header is passed, then the version is determined automatically.
We recommend to use this option only for tests purposes, not in production.
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.
It also allows the kernel to act like a load balancer to distribute incoming connections between entry points.
More information [here](#reuseport). | false | No | +| `transport.`
`respondingTimeouts.`
`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`.
If zero, no timeout exists.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds. | 60s (seconds) | No | +| `transport.`
`respondingTimeouts.`
`writeTimeout` | Maximum duration before timing out writes of the response.
It covers the time from the end of the request header read to the end of the response write.
If zero, no timeout exists.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds. | 0s (seconds) | No | +| `transport.`
`respondingTimeouts.`
`idleTimeout` | Maximum duration an idle (keep-alive) connection will remain idle before closing itself.
If zero, no timeout exists
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds | 180s (seconds) | No | +| `transport.`
`lifeCycle.`
`graceTimeOut` | Set the duration to give active requests a chance to finish before Traefik stops.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds
In this time frame no new requests are accepted. | 10s (seconds) | No | +| `transport.`
`lifeCycle.`
`requestAcceptGraceTimeout` | Set the duration to keep accepting requests prior to initiating the graceful termination period (as defined by the `transportlifeCycle.graceTimeOut` option).
This option is meant to give downstream load-balancers sufficient time to take Traefik out of rotation.
Can be provided in a format supported by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) or as raw values (digits).
If no units are provided, the value is parsed assuming seconds | 0s (seconds) | No | +| `transport.`
`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).
Zero means no limit. | 0 | No | +| `transport.`
`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.
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. diff --git a/docs/content/reference/routing-configuration/http/router/observability.md b/docs/content/reference/routing-configuration/http/router/observability.md index ecadcaeed..4c9229f1c 100644 --- a/docs/content/reference/routing-configuration/http/router/observability.md +++ b/docs/content/reference/routing-configuration/http/router/observability.md @@ -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. diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index cfc377935..839f38988 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -283,13 +283,16 @@ HTTP/3 configuration. (Default: ```false```) UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) `--entrypoints..observability.accesslogs`: - (Default: ```true```) +Enables access-logs for this entryPoint. (Default: ```true```) `--entrypoints..observability.metrics`: - (Default: ```true```) +Enables metrics for this entryPoint. (Default: ```true```) + +`--entrypoints..observability.traceverbosity`: +Defines the tracing verbosity level for this entryPoint. (Default: ```minimal```) `--entrypoints..observability.tracing`: - (Default: ```true```) +Enables tracing for this entryPoint. (Default: ```true```) `--entrypoints..proxyprotocol`: Proxy-Protocol configuration. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index 7a3f59fcb..f98a205b1 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -283,13 +283,16 @@ Subject alternative names. Default TLS options for the routers linked to the entry point. `TRAEFIK_ENTRYPOINTS__OBSERVABILITY_ACCESSLOGS`: - (Default: ```true```) +Enables access-logs for this entryPoint. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__OBSERVABILITY_METRICS`: - (Default: ```true```) +Enables metrics for this entryPoint. (Default: ```true```) + +`TRAEFIK_ENTRYPOINTS__OBSERVABILITY_TRACEVERBOSITY`: +Defines the tracing verbosity level for this entryPoint. (Default: ```minimal```) `TRAEFIK_ENTRYPOINTS__OBSERVABILITY_TRACING`: - (Default: ```true```) +Enables tracing for this entryPoint. (Default: ```true```) `TRAEFIK_ENTRYPOINTS__PROXYPROTOCOL`: Proxy-Protocol configuration. (Default: ```false```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 515a55687..c9746ce47 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -80,8 +80,9 @@ timeout = "42s" [entryPoints.EntryPoint0.observability] accessLogs = true - tracing = true metrics = true + tracing = true + traceVerbosity = "foobar" [providers] providersThrottleDuration = "42s" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index f36478611..c6ad473f5 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -94,8 +94,9 @@ entryPoints: timeout: 42s observability: accessLogs: true - tracing: true metrics: true + tracing: true + traceVerbosity: foobar providers: providersThrottleDuration: 42s docker: diff --git a/integration/access_log_test.go b/integration/access_log_test.go index e1c4003e9..2f480b524 100644 --- a/integration/access_log_test.go +++ b/integration/access_log_test.go @@ -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() } diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index d3f71b5f9..22a08a7d7 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -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: diff --git a/integration/fixtures/tracing/simple-opentelemetry.toml b/integration/fixtures/tracing/simple-opentelemetry.toml index a8643ebd7..61aaebfa8 100644 --- a/integration/fixtures/tracing/simple-opentelemetry.toml +++ b/integration/fixtures/tracing/simple-opentelemetry.toml @@ -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" diff --git a/integration/resources/compose/access_log.yml b/integration/resources/compose/access_log.yml index a5e4f5d44..1447f4cba 100644 --- a/integration/resources/compose/access_log.yml +++ b/integration/resources/compose/access_log.yml @@ -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 diff --git a/integration/testdata/rawdata-consul.json b/integration/testdata/rawdata-consul.json index 90806c2ac..9b27203a7 100644 --- a/integration/testdata/rawdata-consul.json +++ b/integration/testdata/rawdata-consul.json @@ -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": [ { diff --git a/integration/testdata/rawdata-crd-label-selector.json b/integration/testdata/rawdata-crd-label-selector.json index 8ff23a25a..88219a627 100644 --- a/integration/testdata/rawdata-crd-label-selector.json +++ b/integration/testdata/rawdata-crd-label-selector.json @@ -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": [ diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index 334b6c698..9f1b2d33c 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -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" diff --git a/integration/testdata/rawdata-etcd.json b/integration/testdata/rawdata-etcd.json index f03d1b67e..360488433 100644 --- a/integration/testdata/rawdata-etcd.json +++ b/integration/testdata/rawdata-etcd.json @@ -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": [ { diff --git a/integration/testdata/rawdata-gateway.json b/integration/testdata/rawdata-gateway.json index b6206c121..8a6e1bc8c 100644 --- a/integration/testdata/rawdata-gateway.json +++ b/integration/testdata/rawdata-gateway.json @@ -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": [ diff --git a/integration/testdata/rawdata-ingress-label-selector.json b/integration/testdata/rawdata-ingress-label-selector.json index 23305a847..a4081171c 100644 --- a/integration/testdata/rawdata-ingress-label-selector.json +++ b/integration/testdata/rawdata-ingress-label-selector.json @@ -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": [ diff --git a/integration/testdata/rawdata-ingress.json b/integration/testdata/rawdata-ingress.json index ba2ad706b..a317099fb 100644 --- a/integration/testdata/rawdata-ingress.json +++ b/integration/testdata/rawdata-ingress.json @@ -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": [ diff --git a/integration/testdata/rawdata-ingressclass-disabled.json b/integration/testdata/rawdata-ingressclass-disabled.json index a26369282..5f56a981f 100644 --- a/integration/testdata/rawdata-ingressclass-disabled.json +++ b/integration/testdata/rawdata-ingressclass-disabled.json @@ -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": [ diff --git a/integration/testdata/rawdata-ingressclass.json b/integration/testdata/rawdata-ingressclass.json index 05649bfa1..c860c8862 100644 --- a/integration/testdata/rawdata-ingressclass.json +++ b/integration/testdata/rawdata-ingressclass.json @@ -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": [ diff --git a/integration/testdata/rawdata-redis.json b/integration/testdata/rawdata-redis.json index 381d92df5..84e094947 100644 --- a/integration/testdata/rawdata-redis.json +++ b/integration/testdata/rawdata-redis.json @@ -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" diff --git a/integration/testdata/rawdata-zk.json b/integration/testdata/rawdata-zk.json index 265afbdbc..3401d4c32 100644 --- a/integration/testdata/rawdata-zk.json +++ b/integration/testdata/rawdata-zk.json @@ -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": [ { diff --git a/integration/tracing_test.go b/integration/tracing_test.go index d44ed0d0b..9b9fa0faf 100644 --- a/integration/tracing_test.go +++ b/integration/tracing_test.go @@ -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", diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index a867c882b..ef6a896f1 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -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 diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 9cf39ba40..97f8907e0 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -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 } diff --git a/pkg/config/runtime/runtime.go b/pkg/config/runtime/runtime.go index d67233887..4034fa077 100644 --- a/pkg/config/runtime/runtime.go +++ b/pkg/config/runtime/runtime.go @@ -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 { diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index d98cdb1b3..3ac50e048 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -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 } diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index 6ec8ea4c9..246556195 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -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{ diff --git a/pkg/middlewares/accesslog/logger_formatters_test.go b/pkg/middlewares/accesslog/logger_formatters_test.go index 028e3dbf7..7b00bacba 100644 --- a/pkg/middlewares/accesslog/logger_formatters_test.go +++ b/pkg/middlewares/accesslog/logger_formatters_test.go @@ -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) { diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 4daf1a5a5..7babf8121 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -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) diff --git a/pkg/middlewares/addprefix/add_prefix.go b/pkg/middlewares/addprefix/add_prefix.go index 46f8d98d0..b698cc9ce 100644 --- a/pkg/middlewares/addprefix/add_prefix.go +++ b/pkg/middlewares/addprefix/add_prefix.go @@ -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) { diff --git a/pkg/middlewares/auth/basic_auth.go b/pkg/middlewares/auth/basic_auth.go index e6c175bcb..863c968f3 100644 --- a/pkg/middlewares/auth/basic_auth.go +++ b/pkg/middlewares/auth/basic_auth.go @@ -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) { diff --git a/pkg/middlewares/auth/digest_auth.go b/pkg/middlewares/auth/digest_auth.go index 25c22865f..af64a134b 100644 --- a/pkg/middlewares/auth/digest_auth.go +++ b/pkg/middlewares/auth/digest_auth.go @@ -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) { diff --git a/pkg/middlewares/auth/forward.go b/pkg/middlewares/auth/forward.go index 8520e2357..d30d33e2a 100644 --- a/pkg/middlewares/auth/forward.go +++ b/pkg/middlewares/auth/forward.go @@ -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() diff --git a/pkg/middlewares/auth/forward_test.go b/pkg/middlewares/auth/forward_test.go index 2bd062370..d3cdebd36 100644 --- a/pkg/middlewares/auth/forward_test.go +++ b/pkg/middlewares/auth/forward_test.go @@ -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") diff --git a/pkg/middlewares/buffering/buffering.go b/pkg/middlewares/buffering/buffering.go index ec9100a98..cb0fa3414 100644 --- a/pkg/middlewares/buffering/buffering.go +++ b/pkg/middlewares/buffering/buffering.go @@ -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) { diff --git a/pkg/middlewares/circuitbreaker/circuit_breaker.go b/pkg/middlewares/circuitbreaker/circuit_breaker.go index ceaa93de4..c9b3546e3 100644 --- a/pkg/middlewares/circuitbreaker/circuit_breaker.go +++ b/pkg/middlewares/circuitbreaker/circuit_breaker.go @@ -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) { diff --git a/pkg/middlewares/compress/compress.go b/pkg/middlewares/compress/compress.go index 0bb3788d2..4ab312cc2 100644 --- a/pkg/middlewares/compress/compress.go +++ b/pkg/middlewares/compress/compress.go @@ -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) { diff --git a/pkg/middlewares/customerrors/custom_errors.go b/pkg/middlewares/customerrors/custom_errors.go index 3cd866a74..d9c95ea6d 100644 --- a/pkg/middlewares/customerrors/custom_errors.go +++ b/pkg/middlewares/customerrors/custom_errors.go @@ -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) { diff --git a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go index 94d43211a..8808f3e7e 100644 --- a/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go +++ b/pkg/middlewares/gatewayapi/headermodifier/request_header_modifier.go @@ -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) { diff --git a/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go index 2d55b686b..e47d23758 100644 --- a/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go +++ b/pkg/middlewares/gatewayapi/headermodifier/response_header_modifier.go @@ -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) { diff --git a/pkg/middlewares/gatewayapi/redirect/request_redirect.go b/pkg/middlewares/gatewayapi/redirect/request_redirect.go index e2d32e518..1305a2372 100644 --- a/pkg/middlewares/gatewayapi/redirect/request_redirect.go +++ b/pkg/middlewares/gatewayapi/redirect/request_redirect.go @@ -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) { diff --git a/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go b/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go index 2960a5583..bcddb8137 100644 --- a/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go +++ b/pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go @@ -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) { diff --git a/pkg/middlewares/headers/headers.go b/pkg/middlewares/headers/headers.go index 861d1066d..cba8b6401 100644 --- a/pkg/middlewares/headers/headers.go +++ b/pkg/middlewares/headers/headers.go @@ -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) { diff --git a/pkg/middlewares/headers/headers_test.go b/pkg/middlewares/headers/headers_test.go index 6ce7383cf..df71e6896 100644 --- a/pkg/middlewares/headers/headers_test.go +++ b/pkg/middlewares/headers/headers_test.go @@ -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. diff --git a/pkg/middlewares/inflightreq/inflight_req.go b/pkg/middlewares/inflightreq/inflight_req.go index c05193607..65a7457d3 100644 --- a/pkg/middlewares/inflightreq/inflight_req.go +++ b/pkg/middlewares/inflightreq/inflight_req.go @@ -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) { diff --git a/pkg/middlewares/ipallowlist/ip_allowlist.go b/pkg/middlewares/ipallowlist/ip_allowlist.go index 46f17c74c..6d9cf0232 100644 --- a/pkg/middlewares/ipallowlist/ip_allowlist.go +++ b/pkg/middlewares/ipallowlist/ip_allowlist.go @@ -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) { diff --git a/pkg/middlewares/ipwhitelist/ip_whitelist.go b/pkg/middlewares/ipwhitelist/ip_whitelist.go index d8e3dadb3..7458b4de6 100644 --- a/pkg/middlewares/ipwhitelist/ip_whitelist.go +++ b/pkg/middlewares/ipwhitelist/ip_whitelist.go @@ -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) { diff --git a/pkg/middlewares/metrics/metrics.go b/pkg/middlewares/metrics/metrics.go index e8a1c6dba..f7eb3b8ff 100644 --- a/pkg/middlewares/metrics/metrics.go +++ b/pkg/middlewares/metrics/metrics.go @@ -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 } diff --git a/pkg/middlewares/observability/entrypoint.go b/pkg/middlewares/observability/entrypoint.go index 64b89b921..e9c77c160 100644 --- a/pkg/middlewares/observability/entrypoint.go +++ b/pkg/middlewares/observability/entrypoint.go @@ -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) diff --git a/pkg/middlewares/observability/middleware.go b/pkg/middlewares/observability/middleware.go index 51e4b6d50..b44175d40 100644 --- a/pkg/middlewares/observability/middleware.go +++ b/pkg/middlewares/observability/middleware.go @@ -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) diff --git a/pkg/middlewares/observability/observability.go b/pkg/middlewares/observability/observability.go index 1ee9f3b99..1490a1e8e 100644 --- a/pkg/middlewares/observability/observability.go +++ b/pkg/middlewares/observability/observability.go @@ -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{}) { diff --git a/pkg/middlewares/observability/router.go b/pkg/middlewares/observability/router.go index 5339726ff..41740443c 100644 --- a/pkg/middlewares/observability/router.go +++ b/pkg/middlewares/observability/router.go @@ -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() diff --git a/pkg/middlewares/observability/semconv.go b/pkg/middlewares/observability/semconv.go index 51f4480b5..d23363b4a 100644 --- a/pkg/middlewares/observability/semconv.go +++ b/pkg/middlewares/observability/semconv.go @@ -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 } diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go index 40d3faf62..06a39e70b 100644 --- a/pkg/middlewares/observability/semconv_test.go +++ b/pkg/middlewares/observability/semconv_test.go @@ -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{} diff --git a/pkg/middlewares/observability/service.go b/pkg/middlewares/observability/service.go index cacd3ef1b..c914c09cc 100644 --- a/pkg/middlewares/observability/service.go +++ b/pkg/middlewares/observability/service.go @@ -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() diff --git a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go index cadc375af..6f892a779 100644 --- a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go +++ b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go @@ -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) { diff --git a/pkg/middlewares/ratelimiter/rate_limiter.go b/pkg/middlewares/ratelimiter/rate_limiter.go index 043974d47..fcc553e15 100755 --- a/pkg/middlewares/ratelimiter/rate_limiter.go +++ b/pkg/middlewares/ratelimiter/rate_limiter.go @@ -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) { diff --git a/pkg/middlewares/redirect/redirect.go b/pkg/middlewares/redirect/redirect.go index 25ca6a2ac..d8499d1aa 100644 --- a/pkg/middlewares/redirect/redirect.go +++ b/pkg/middlewares/redirect/redirect.go @@ -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) { diff --git a/pkg/middlewares/replacepath/replace_path.go b/pkg/middlewares/replacepath/replace_path.go index 9c8e404d4..d211b83dd 100644 --- a/pkg/middlewares/replacepath/replace_path.go +++ b/pkg/middlewares/replacepath/replace_path.go @@ -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) { diff --git a/pkg/middlewares/replacepathregex/replace_path_regex.go b/pkg/middlewares/replacepathregex/replace_path_regex.go index f04e9d9a0..42cd79404 100644 --- a/pkg/middlewares/replacepathregex/replace_path_regex.go +++ b/pkg/middlewares/replacepathregex/replace_path_regex.go @@ -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) { diff --git a/pkg/middlewares/retry/retry.go b/pkg/middlewares/retry/retry.go index 068030777..20263d897 100644 --- a/pkg/middlewares/retry/retry.go +++ b/pkg/middlewares/retry/retry.go @@ -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() } diff --git a/pkg/middlewares/stripprefix/strip_prefix.go b/pkg/middlewares/stripprefix/strip_prefix.go index 8483f5100..1632814ce 100644 --- a/pkg/middlewares/stripprefix/strip_prefix.go +++ b/pkg/middlewares/stripprefix/strip_prefix.go @@ -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) { diff --git a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go index 71de1ad28..a38752659 100644 --- a/pkg/middlewares/stripprefixregex/strip_prefix_regex.go +++ b/pkg/middlewares/stripprefixregex/strip_prefix_regex.go @@ -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) { diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go index bda404889..6879f2ae1 100644 --- a/pkg/provider/kubernetes/ingress/annotations_test.go +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -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, }, }, }, diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index 6cf0113a4..acd7400a4 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -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, }, }, }, diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 22efe451d..544c1a2c5 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -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, } } diff --git a/pkg/proxy/httputil/builder.go b/pkg/proxy/httputil/builder.go index 64360517a..cb591c8db 100644 --- a/pkg/proxy/httputil/builder.go +++ b/pkg/proxy/httputil/builder.go @@ -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 } diff --git a/pkg/proxy/httputil/builder_test.go b/pkg/proxy/httputil/builder_test.go index f7ff93902..e45a871b6 100644 --- a/pkg/proxy/httputil/builder_test.go +++ b/pkg/proxy/httputil/builder_test.go @@ -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)) diff --git a/pkg/proxy/httputil/observability.go b/pkg/proxy/httputil/observability.go index 8fa3382e3..1a8d7b1f1 100644 --- a/pkg/proxy/httputil/observability.go +++ b/pkg/proxy/httputil/observability.go @@ -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 } diff --git a/pkg/proxy/httputil/observability_test.go b/pkg/proxy/httputil/observability_test.go index 67d585a8f..02a4f084f 100644 --- a/pkg/proxy/httputil/observability_test.go +++ b/pkg/proxy/httputil/observability_test.go @@ -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) diff --git a/pkg/proxy/httputil/proxy_websocket_test.go b/pkg/proxy/httputil/proxy_websocket_test.go index 48296f955..7abfc39f6 100644 --- a/pkg/proxy/httputil/proxy_websocket_test.go +++ b/pkg/proxy/httputil/proxy_websocket_test.go @@ -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) { diff --git a/pkg/proxy/smart_builder.go b/pkg/proxy/smart_builder.go index 08b247c53..ad9d14d56 100644 --- a/pkg/proxy/smart_builder.go +++ b/pkg/proxy/smart_builder.go @@ -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) } diff --git a/pkg/proxy/smart_builder_test.go b/pkg/proxy/smart_builder_test.go index c03bd19f3..d1c29ddd8 100644 --- a/pkg/proxy/smart_builder_test.go +++ b/pkg/proxy/smart_builder_test.go @@ -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() diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index 2a0c1bd4f..9c35b00ac 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -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 } } } diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index 182815a39..4c202fdf8 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -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, }, }, }, diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index 9eebfd31c..624a3c9c9 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -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 } diff --git a/pkg/server/middleware/observability.go b/pkg/server/middleware/observability.go index d279be902..82152c7fc 100644 --- a/pkg/server/middleware/observability.go +++ b/pkg/server/middleware/observability.go @@ -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 +} diff --git a/pkg/server/middleware/plugins.go b/pkg/server/middleware/plugins.go index eacf8fba1..0529ca190 100644 --- a/pkg/server/middleware/plugins.go +++ b/pkg/server/middleware/plugins.go @@ -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 } diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index 3abd239e5..44f6950d7 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -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() -} diff --git a/pkg/server/router/router_test.go b/pkg/server/router/router_test.go index 50811be7c..1458b2e99 100644 --- a/pkg/server/router/router_test.go +++ b/pkg/server/router/router_test.go @@ -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 } diff --git a/pkg/server/routerfactory_test.go b/pkg/server/routerfactory_test.go index 8a536890f..ba77f5e31 100644 --- a/pkg/server/routerfactory_test.go +++ b/pkg/server/routerfactory_test.go @@ -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 } diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index ea8ea6e48..41db759a7 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -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 { diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 82cc96e36..b1bc6d8e9 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -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) diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go index ee3dbc975..8ab73b6b6 100644 --- a/pkg/testhelpers/config.go +++ b/pkg/testhelpers/config.go @@ -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, } } } diff --git a/pkg/types/tracing.go b/pkg/types/tracing.go index c3d83deec..0ee66ea3c 100644 --- a/pkg/types/tracing.go +++ b/pkg/types/tracing.go @@ -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"` diff --git a/pkg/types/tracing_test.go b/pkg/types/tracing_test.go new file mode 100644 index 000000000..77f764871 --- /dev/null +++ b/pkg/types/tracing_test.go @@ -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)) + }) + } +} From 7b78128d4ee448e3ca826186a9651820921c7d30 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Fri, 18 Jul 2025 17:08:04 +0200 Subject: [PATCH 03/18] Add resourceAttributes option to OTel metrics Co-authored-by: Romain --- docs/content/observability/access-logs.md | 48 +++++++++++ docs/content/observability/logs.md | 48 +++++++++++ .../observability/metrics/opentelemetry.md | 27 ++++++- .../observability/logs-and-accesslogs.md | 80 ++++++++++--------- .../observability/metrics.md | 48 +++++------ .../observability/tracing.md | 48 +++++------ .../reference/static-configuration/cli-ref.md | 11 ++- .../reference/static-configuration/env-ref.md | 11 ++- .../reference/static-configuration/file.toml | 3 + .../reference/static-configuration/file.yaml | 3 + pkg/config/static/static_config.go | 2 +- pkg/metrics/otel.go | 17 +++- pkg/types/logs.go | 2 +- pkg/types/metrics.go | 13 +-- 14 files changed, 256 insertions(+), 105 deletions(-) diff --git a/docs/content/observability/access-logs.md b/docs/content/observability/access-logs.md index 0ff649fc9..6b6e4e20b 100644 --- a/docs/content/observability/access-logs.md +++ b/docs/content/observability/access-logs.md @@ -340,6 +340,54 @@ accesslog: The OpenTelemetry Logger exporter will export access logs to the collector using HTTPS by default to https://localhost:4318/v1/logs, see the [gRPC Section](#grpc-configuration) to use gRPC. +### `serviceName` + +_Optional, Default="traefik"_ + +Defines the service name resource attribute. + +```yaml tab="File (YAML)" +accesslog: + otlp: + serviceName: name +``` + +```toml tab="File (TOML)" +[accesslog] + [accesslog.otlp] + serviceName = "name" +``` + +```bash tab="CLI" +--accesslog.otlp.serviceName=name +``` + +### `ressourceAttributes` + +_Optional, Default=empty_ + +Defines additional resource attributes to be sent to the collector. + +```yaml tab="File (YAML)" +accesslog: + otlp: + resourceAttributes: + attr1: foo + attr2: bar +``` + +```toml tab="File (TOML)" +[accesslog] + [accesslog.otlp.resourceAttributes] + attr1 = "foo" + attr2 = "bar" +``` + +```bash tab="CLI" +--accesslog.otlp.resourceAttributes.attr1=foo +--accesslog.otlp.resourceAttributes.attr2=bar +``` + ### HTTP configuration _Optional_ diff --git a/docs/content/observability/logs.md b/docs/content/observability/logs.md index 51be940b8..81d007cba 100644 --- a/docs/content/observability/logs.md +++ b/docs/content/observability/logs.md @@ -219,6 +219,54 @@ log: The OpenTelemetry Logger exporter will export logs to the collector using HTTPS by default to https://localhost:4318/v1/logs, see the [gRPC Section](#grpc-configuration) to use gRPC. +### `serviceName` + +_Optional, Default="traefik"_ + +Defines the service name resource attribute. + +```yaml tab="File (YAML)" +log: + otlp: + serviceName: name +``` + +```toml tab="File (TOML)" +[log] + [log.otlp] + serviceName = "name" +``` + +```bash tab="CLI" +--log.otlp.serviceName=name +``` + +### `ressourceAttributes` + +_Optional, Default=empty_ + +Defines additional resource attributes to be sent to the collector. + +```yaml tab="File (YAML)" +log: + otlp: + resourceAttributes: + attr1: foo + attr2: bar +``` + +```toml tab="File (TOML)" +[log] + [log.otlp.resourceAttributes] + attr1 = "foo" + attr2 = "bar" +``` + +```bash tab="CLI" +--log.otlp.resourceAttributes.attr1=foo +--log.otlp.resourceAttributes.attr2=bar +``` + ### HTTP configuration _Optional_ diff --git a/docs/content/observability/metrics/opentelemetry.md b/docs/content/observability/metrics/opentelemetry.md index 0bcb96587..90cc8f81c 100644 --- a/docs/content/observability/metrics/opentelemetry.md +++ b/docs/content/observability/metrics/opentelemetry.md @@ -143,7 +143,7 @@ metrics: _Optional, Default="traefik"_ -OTEL service name to use. +Defines the service name resource attribute. ```yaml tab="File (YAML)" metrics: @@ -160,6 +160,31 @@ metrics: ```bash tab="CLI" --metrics.otlp.serviceName=name ``` +#### `ressourceAttributes` + +_Optional, Default=empty_ + +Defines additional resource attributes to be sent to the collector. + +```yaml tab="File (YAML)" +metrics: + otlp: + resourceAttributes: + attr1: foo + attr2: bar +``` + +```toml tab="File (TOML)" +[metrics] + [metrics.otlp.resourceAttributes] + attr1 = "foo" + attr2 = "bar" +``` + +```bash tab="CLI" +--metrics.otlp.resourceAttributes.attr1=foo +--metrics.otlp.resourceAttributes.attr2=bar +``` ### HTTP configuration diff --git a/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md b/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md index d67feb027..afd2b07ae 100644 --- a/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md +++ b/docs/content/reference/install-configuration/observability/logs-and-accesslogs.md @@ -96,25 +96,27 @@ log: #### Configuration Options -| Field | Description | Default | Required | -|:-----------|:-----------------------------------------------------------------------------|:--------|:---------| -| `log.otlp.http` | This instructs the exporter to send logs to the OpenTelemetry Collector using HTTP.| | No | -| `log.otlp.http.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`://:`) | `https://localhost:4318/v1/logs` | No | -| `log.otlp.http.headers` | Additional headers sent with logs by the exporter to the OpenTelemetry Collector. | [ ] | No | -| `log.otlp.http.tls` | Defines the Client TLS configuration used by the exporter to send logs to the OpenTelemetry Collector. | | No | -| `log.otlp.http.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | -| `log.otlp.http.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | -| `log.otlp.http.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | -| `log.otlp.http.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | -| `log.otlp.grpc` | This instructs the exporter to send logs to the OpenTelemetry Collector using gRPC.| | No | -| `log.otlp.grpc.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`:`) | `localhost:4317` | No | -| `log.otlp.grpc.headers` | Additional headers sent with logs by the exporter to the OpenTelemetry Collector. | [ ] | No | -| `log.otlp.grpc.insecure` | Instructs the exporter to send logs to the OpenTelemetry Collector using an insecure protocol. | false | No | -| `log.otlp.grpc.tls` | Defines the Client TLS configuration used by the exporter to send logs to the OpenTelemetry Collector. | | No | -| `log.otlp.grpc.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | -| `log.otlp.grpc.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | -| `log.otlp.grpc.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | -| `log.otlp.grpc.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | +| Field | Description | Default | Required | +|:---------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------|:---------| +| `log.otlp.serviceName` | Service name used in selected backend. | "traefik" | No | +| `log.otlp.resourceAttributes` | Defines additional resource attributes to be sent to the collector. | [] | No | +| `log.otlp.http` | This instructs the exporter to send logs to the OpenTelemetry Collector using HTTP. | | No | +| `log.otlp.http.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`://:`) | `https://localhost:4318/v1/logs` | No | +| `log.otlp.http.headers` | Additional headers sent with logs by the exporter to the OpenTelemetry Collector. | [ ] | No | +| `log.otlp.http.tls` | Defines the Client TLS configuration used by the exporter to send logs to the OpenTelemetry Collector. | | No | +| `log.otlp.http.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | +| `log.otlp.http.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | +| `log.otlp.http.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | +| `log.otlp.http.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | +| `log.otlp.grpc` | This instructs the exporter to send logs to the OpenTelemetry Collector using gRPC. | | No | +| `log.otlp.grpc.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`:`) | `localhost:4317` | No | +| `log.otlp.grpc.headers` | Additional headers sent with logs by the exporter to the OpenTelemetry Collector. | [ ] | No | +| `log.otlp.grpc.insecure` | Instructs the exporter to send logs to the OpenTelemetry Collector using an insecure protocol. | false | No | +| `log.otlp.grpc.tls` | Defines the Client TLS configuration used by the exporter to send logs to the OpenTelemetry Collector. | | No | +| `log.otlp.grpc.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | +| `log.otlp.grpc.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | +| `log.otlp.grpc.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | +| `log.otlp.grpc.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | ## AccessLogs @@ -256,25 +258,27 @@ accesslog: #### Configuration Options -| Field | Description | Default | Required | -|:-----------|:--------------------------|:--------|:---------| -| `accesslog.otlp.http` | This instructs the exporter to send access logs to the OpenTelemetry Collector using HTTP.| | No | -| `accesslog.otlp.http.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`://:`) | `https://localhost:4318/v1/logs` | No | -| `accesslog.otlp.http.headers` | Additional headers sent with access logs by the exporter to the OpenTelemetry Collector. | [ ] | No | -| `accesslog.otlp.http.tls` | Defines the Client TLS configuration used by the exporter to send access logs to the OpenTelemetry Collector. | | No | -| `accesslog.otlp.http.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | -| `accesslog.otlp.http.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | -| `accesslog.otlp.http.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | -| `accesslog.otlp.http.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | -| `accesslog.otlp.grpc` | This instructs the exporter to send access logs to the OpenTelemetry Collector using gRPC.| | No | -| `accesslog.otlp.grpc.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`:`) | `localhost:4317` | No | -| `accesslog.otlp.grpc.headers` | Additional headers sent with access logs by the exporter to the OpenTelemetry Collector. | [ ] | No | -| `accesslog.otlp.grpc.insecure` | Instructs the exporter to send access logs to the OpenTelemetry Collector using an insecure protocol. | false | No | -| `accesslog.otlp.grpc.tls` | Defines the Client TLS configuration used by the exporter to send access logs to the OpenTelemetry Collector. | | No | -| `accesslog.otlp.grpc.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | -| `accesslog.otlp.grpc.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | -| `accesslog.otlp.grpc.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | -| `accesslog.otlp.grpc.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | +| Field | Description | Default | Required | +|:---------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------|:---------| +| `accesslog.otlp.serviceName` | Defines the service name resource attribute. | "traefik" | No | +| `accesslog.otlp.resourceAttributes` | Defines additional resource attributes to be sent to the collector. | [] | No | +| `accesslog.otlp.http` | This instructs the exporter to send access logs to the OpenTelemetry Collector using HTTP. | | No | +| `accesslog.otlp.http.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`://:`) | `https://localhost:4318/v1/logs` | No | +| `accesslog.otlp.http.headers` | Additional headers sent with access logs by the exporter to the OpenTelemetry Collector. | [ ] | No | +| `accesslog.otlp.http.tls` | Defines the Client TLS configuration used by the exporter to send access logs to the OpenTelemetry Collector. | | No | +| `accesslog.otlp.http.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | +| `accesslog.otlp.http.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | +| `accesslog.otlp.http.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | +| `accesslog.otlp.http.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | +| `accesslog.otlp.grpc` | This instructs the exporter to send access logs to the OpenTelemetry Collector using gRPC. | | No | +| `accesslog.otlp.grpc.endpoint` | The endpoint of the OpenTelemetry Collector. (format=`:`) | `localhost:4317` | No | +| `accesslog.otlp.grpc.headers` | Additional headers sent with access logs by the exporter to the OpenTelemetry Collector. | [ ] | No | +| `accesslog.otlp.grpc.insecure` | Instructs the exporter to send access logs to the OpenTelemetry Collector using an insecure protocol. | false | No | +| `accesslog.otlp.grpc.tls` | Defines the Client TLS configuration used by the exporter to send access logs to the OpenTelemetry Collector. | | No | +| `accesslog.otlp.grpc.tls.ca` | The path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | | No | +| `accesslog.otlp.grpc.tls.cert` | The path to the certificate to use for the OpenTelemetry Collector. | | No | +| `accesslog.otlp.grpc.tls.key` | The path to the key to use for the OpenTelemetry Collector. | | No | +| `accesslog.otlp.grpc.tls.insecureSkipVerify` | Instructs the OpenTelemetry Collector to accept any certificate presented by the server regardless of the hostname in the certificate. | false | No | ### CLF format fields diff --git a/docs/content/reference/install-configuration/observability/metrics.md b/docs/content/reference/install-configuration/observability/metrics.md index c2c6f4bd0..6373ff3ab 100644 --- a/docs/content/reference/install-configuration/observability/metrics.md +++ b/docs/content/reference/install-configuration/observability/metrics.md @@ -60,29 +60,31 @@ metrics: ### Configuration Options -| Field | Description | Default | Required | -|:-----------|---------------|:--------|:---------| -| `metrics.addInternals` | Enables metrics for internal resources (e.g.: `ping@internal`). | false | No | -| `metrics.otlp.addEntryPointsLabels` | Enable metrics on entry points. | true | No | -| `metrics.otlp.addRoutersLabels` | Enable metrics on routers. | false | No | -| `metrics.otlp.addServicesLabels` | Enable metrics on services.| true | No | -| `metrics.otlp.explicitBoundaries` | Explicit boundaries for Histogram data points. | ".005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10" | No | -| `metrics.otlp.pushInterval` | Interval at which metrics are sent to the OpenTelemetry Collector. | 10s | No | -| `metrics.otlp.http` | This instructs the exporter to send the metrics to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | -| `metrics.otlp.http.endpoint` | URL of the OpenTelemetry Collector to send metrics to.
Format="`://:`" | "http://localhost:4318/v1/metrics" | Yes | -| `metrics.otlp.http.headers` | Additional headers sent with metrics by the exporter to the OpenTelemetry Collector. | - | No | -| `metrics.otlp.http.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector,
it defaults to the system bundle. | "" | No | -| `metrics.otlp.http.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector.
When using this option, setting the `key` option is required. | "" | No | -| `metrics.otlp.http.tls.key` | This instructs the exporter to send the metrics to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | -| `metrics.otlp.http.tls.insecureskipverify` | Allow the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | -| `metrics.otlp.grpc` | This instructs the exporter to send metrics to the OpenTelemetry Collector using gRPC. | null/false | No | -| `metrics.otlp.grpc.endpoint` | Address of the OpenTelemetry Collector to send metrics to.
Format="`:`" | "localhost:4317" | Yes | -| `metrics.otlp.grpc.headers` | Additional headers sent with metrics by the exporter to the OpenTelemetry Collector. | - | No | -| `metrics.otlp.http.grpc.insecure` |Allows exporter to send metrics to the OpenTelemetry Collector without using a secured protocol. | false | Yes | -| `metrics.otlp.grpc.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector,
it defaults to the system bundle. | - | No | -| `metrics.otlp.grpc.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector.
When using this option, setting the `key` option is required. | - | No | -| `metrics.otlp.grpc.tls.key` | This instructs the exporter to send the metrics to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | -| `metrics.otlp.grpc.tls.insecureskipverify` | Allow the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | +| Field | Description | Default | Required | +|:-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------|:---------| +| `metrics.addInternals` | Enables metrics for internal resources (e.g.: `ping@internal`). | false | No | +| `metrics.otlp.serviceName` | Defines the service name resource attribute. | "traefik" | No | +| `metrics.otlp.resourceAttributes` | Defines additional resource attributes to be sent to the collector. | [] | No | +| `metrics.otlp.addEntryPointsLabels` | Enable metrics on entry points. | true | No | +| `metrics.otlp.addRoutersLabels` | Enable metrics on routers. | false | No | +| `metrics.otlp.addServicesLabels` | Enable metrics on services. | true | No | +| `metrics.otlp.explicitBoundaries` | Explicit boundaries for Histogram data points. | ".005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10" | No | +| `metrics.otlp.pushInterval` | Interval at which metrics are sent to the OpenTelemetry Collector. | 10s | No | +| `metrics.otlp.http` | This instructs the exporter to send the metrics to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | +| `metrics.otlp.http.endpoint` | URL of the OpenTelemetry Collector to send metrics to.
Format="`://:`" | "http://localhost:4318/v1/metrics" | Yes | +| `metrics.otlp.http.headers` | Additional headers sent with metrics by the exporter to the OpenTelemetry Collector. | - | No | +| `metrics.otlp.http.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector,
it defaults to the system bundle. | "" | No | +| `metrics.otlp.http.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector.
When using this option, setting the `key` option is required. | "" | No | +| `metrics.otlp.http.tls.key` | This instructs the exporter to send the metrics to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | +| `metrics.otlp.http.tls.insecureskipverify` | Allow the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | +| `metrics.otlp.grpc` | This instructs the exporter to send metrics to the OpenTelemetry Collector using gRPC. | null/false | No | +| `metrics.otlp.grpc.endpoint` | Address of the OpenTelemetry Collector to send metrics to.
Format="`:`" | "localhost:4317" | Yes | +| `metrics.otlp.grpc.headers` | Additional headers sent with metrics by the exporter to the OpenTelemetry Collector. | - | No | +| `metrics.otlp.http.grpc.insecure` | Allows exporter to send metrics to the OpenTelemetry Collector without using a secured protocol. | false | Yes | +| `metrics.otlp.grpc.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector,
it defaults to the system bundle. | - | No | +| `metrics.otlp.grpc.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector.
When using this option, setting the `key` option is required. | - | No | +| `metrics.otlp.grpc.tls.key` | This instructs the exporter to send the metrics to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | +| `metrics.otlp.grpc.tls.insecureskipverify` | Allow the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | ## Vendors diff --git a/docs/content/reference/install-configuration/observability/tracing.md b/docs/content/reference/install-configuration/observability/tracing.md index f7b4987c1..1085273a7 100644 --- a/docs/content/reference/install-configuration/observability/tracing.md +++ b/docs/content/reference/install-configuration/observability/tracing.md @@ -36,27 +36,27 @@ tracing: {} ## Configuration Options -| Field | Description | Default | Required | -|:-------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:---------| -| `tracing.addInternals` | Enables tracing for internal resources (e.g.: `ping@internal`). | false | No | -| `tracing.serviceName` | Service name used in selected backend. | "traefik" | No | -| `tracing.sampleRate` | The proportion of requests to trace, specified between 0.0 and 1.0. | 1.0 | No | -| `tracing.resourceAttributes` | Defines additional resource attributes to be sent to the collector. | [] | No | -| `tracing.capturedRequestHeaders` | Defines the list of request headers to add as attributes.
It applies to client and server kind spans.| [] | No | -| `tracing.capturedResponseHeaders` | Defines the list of response headers to add as attributes.
It applies to client and server kind spans.| [] |False | -| `tracing.safeQueryParams` | By default, all query parameters are redacted.
Defines the list of query parameters to not redact. | [] | No | -| `tracing.otlp.http` | This instructs the exporter to send the tracing to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | -| `tracing.otlp.http.endpoint` | URL of the OpenTelemetry Collector to send tracing to.
Format="`://:`" | "http://localhost:4318/v1/tracing" | Yes | -| `tracing.otlp.http.headers` | Additional headers sent with tracing by the exporter to the OpenTelemetry Collector. | | No | -| `tracing.otlp.http.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | "" | No | -| `tracing.otlp.http.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector. When using this option, setting the `key` option is required. | "" | No | -| `tracing.otlp.http.tls.key` | This instructs the exporter to send the tracing to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | ""null/false "" | No | -| `tracing.otlp.http.tls.insecureskipverify` |If `insecureSkipVerify` is `true`, the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | -| `tracing.otlp.grpc` | This instructs the exporter to send tracing to the OpenTelemetry Collector using gRPC. | false | No | -| `tracing.otlp.grpc.endpoint` | Address of the OpenTelemetry Collector to send tracing to.
Format="`:`" | "localhost:4317" | Yes | -| `tracing.otlp.grpc.headers` | Additional headers sent with tracing by the exporter to the OpenTelemetry Collector. | [] | No | -| `tracing.otlp.grpc.insecure` |Allows exporter to send tracing to the OpenTelemetry Collector without using a secured protocol. | false | Yes | -| `tracing.otlp.grpc.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | "" | No | -| `tracing.otlp.grpc.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector. When using this option, setting the `key` option is required. | "" | No | -| `tracing.otlp.grpc.tls.key` | This instructs the exporter to send the tracing to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | ""null/false "" | No | -| `tracing.otlp.grpc.tls.insecureskipverify` |If `insecureSkipVerify` is `true`, the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | +| Field | Description | Default | Required | +|:-------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------|:---------| +| `tracing.addInternals` | Enables tracing for internal resources (e.g.: `ping@internal`). | false | No | +| `tracing.serviceName` | Defines the service name resource attribute. | "traefik" | No | +| `tracing.resourceAttributes` | Defines additional resource attributes to be sent to the collector. | [] | No | +| `tracing.sampleRate` | The proportion of requests to trace, specified between 0.0 and 1.0. | 1.0 | No | +| `tracing.capturedRequestHeaders` | Defines the list of request headers to add as attributes.
It applies to client and server kind spans. | [] | No | +| `tracing.capturedResponseHeaders` | Defines the list of response headers to add as attributes.
It applies to client and server kind spans. | [] | False | +| `tracing.safeQueryParams` | By default, all query parameters are redacted.
Defines the list of query parameters to not redact. | [] | No | +| `tracing.otlp.http` | This instructs the exporter to send the tracing to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | null/false | No | +| `tracing.otlp.http.endpoint` | URL of the OpenTelemetry Collector to send tracing to.
Format="`://:`" | "http://localhost:4318/v1/tracing" | Yes | +| `tracing.otlp.http.headers` | Additional headers sent with tracing by the exporter to the OpenTelemetry Collector. | | No | +| `tracing.otlp.http.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | "" | No | +| `tracing.otlp.http.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector. When using this option, setting the `key` option is required. | "" | No | +| `tracing.otlp.http.tls.key` | This instructs the exporter to send the tracing to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | ""null/false "" | No | +| `tracing.otlp.http.tls.insecureskipverify` | If `insecureSkipVerify` is `true`, the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | +| `tracing.otlp.grpc` | This instructs the exporter to send tracing to the OpenTelemetry Collector using gRPC. | false | No | +| `tracing.otlp.grpc.endpoint` | Address of the OpenTelemetry Collector to send tracing to.
Format="`:`" | "localhost:4317" | Yes | +| `tracing.otlp.grpc.headers` | Additional headers sent with tracing by the exporter to the OpenTelemetry Collector. | [] | No | +| `tracing.otlp.grpc.insecure` | Allows exporter to send tracing to the OpenTelemetry Collector without using a secured protocol. | false | Yes | +| `tracing.otlp.grpc.tls.ca` | Path to the certificate authority used for the secure connection to the OpenTelemetry Collector, it defaults to the system bundle. | "" | No | +| `tracing.otlp.grpc.tls.cert` | Path to the public certificate used for the secure connection to the OpenTelemetry Collector. When using this option, setting the `key` option is required. | "" | No | +| `tracing.otlp.grpc.tls.key` | This instructs the exporter to send the tracing to the OpenTelemetry Collector using HTTP.
Setting the sub-options with their default values. | ""null/false "" | No | +| `tracing.otlp.grpc.tls.insecureskipverify` | If `insecureSkipVerify` is `true`, the TLS connection to the OpenTelemetry Collector accepts any certificate presented by the server regardless of the hostnames it covers. | false | Yes | diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 839f38988..81cfb6e07 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -91,7 +91,7 @@ TLS key Defines additional resource attributes (key:value). `--accesslog.otlp.servicename`: -Set the name for this service. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) `--api`: Enable api/dashboard. (Default: ```false```) @@ -481,7 +481,7 @@ TLS key Defines additional resource attributes (key:value). `--log.otlp.servicename`: -Set the name for this service. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) `--metrics.addinternals`: Enables metrics for internal services (ping, dashboard, etc...). (Default: ```false```) @@ -600,8 +600,11 @@ TLS key `--metrics.otlp.pushinterval`: Period between calls to collect a checkpoint. (Default: ```10```) +`--metrics.otlp.resourceattributes.`: +Defines additional resource attributes (key:value). + `--metrics.otlp.servicename`: -OTEL service name to use. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) `--metrics.prometheus`: Prometheus metrics exporter type. (Default: ```false```) @@ -1414,4 +1417,4 @@ Query params to not redact. Sets the rate between 0.0 and 1.0 of requests to trace. (Default: ```1.000000```) `--tracing.servicename`: -Sets the name for this service. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index f98a205b1..eec38608e 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -91,7 +91,7 @@ TLS key Defines additional resource attributes (key:value). `TRAEFIK_ACCESSLOG_OTLP_SERVICENAME`: -Set the name for this service. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) `TRAEFIK_API`: Enable api/dashboard. (Default: ```false```) @@ -481,7 +481,7 @@ TLS key Defines additional resource attributes (key:value). `TRAEFIK_LOG_OTLP_SERVICENAME`: -Set the name for this service. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) `TRAEFIK_METRICS_ADDINTERNALS`: Enables metrics for internal services (ping, dashboard, etc...). (Default: ```false```) @@ -600,8 +600,11 @@ TLS key `TRAEFIK_METRICS_OTLP_PUSHINTERVAL`: Period between calls to collect a checkpoint. (Default: ```10```) +`TRAEFIK_METRICS_OTLP_RESOURCEATTRIBUTES_`: +Defines additional resource attributes (key:value). + `TRAEFIK_METRICS_OTLP_SERVICENAME`: -OTEL service name to use. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) `TRAEFIK_METRICS_PROMETHEUS`: Prometheus metrics exporter type. (Default: ```false```) @@ -1414,4 +1417,4 @@ Query params to not redact. Sets the rate between 0.0 and 1.0 of requests to trace. (Default: ```1.000000```) `TRAEFIK_TRACING_SERVICENAME`: -Sets the name for this service. (Default: ```traefik```) +Defines the service name resource attribute. (Default: ```traefik```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index c9746ce47..de4fc3601 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -388,6 +388,9 @@ [metrics.otlp.http.headers] name0 = "foobar" name1 = "foobar" + [metrics.otlp.resourceAttributes] + name0 = "foobar" + name1 = "foobar" [ping] entryPoint = "foobar" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index c6ad473f5..f1612dde6 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -429,6 +429,9 @@ metrics: - 42 pushInterval: 42s serviceName: foobar + resourceAttributes: + name0: foobar + name1: foobar ping: entryPoint: foobar manualRouting: true diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index e448ea305..b1cb5ccca 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -205,7 +205,7 @@ func (a *LifeCycle) SetDefaults() { // Tracing holds the tracing configuration. type Tracing struct { - ServiceName string `description:"Sets the name for this service." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"` + ServiceName string `description:"Defines the service name resource attribute." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"` ResourceAttributes map[string]string `description:"Defines additional resource attributes (key:value)." json:"resourceAttributes,omitempty" toml:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty" export:"true"` CapturedRequestHeaders []string `description:"Request headers to add as attributes for server and client spans." json:"capturedRequestHeaders,omitempty" toml:"capturedRequestHeaders,omitempty" yaml:"capturedRequestHeaders,omitempty" export:"true"` CapturedResponseHeaders []string `description:"Response headers to add as attributes for server and client spans." json:"capturedResponseHeaders,omitempty" toml:"capturedResponseHeaders,omitempty" yaml:"capturedResponseHeaders,omitempty" export:"true"` diff --git a/pkg/metrics/otel.go b/pkg/metrics/otel.go index cf49ca008..b29a89f29 100644 --- a/pkg/metrics/otel.go +++ b/pkg/metrics/otel.go @@ -206,15 +206,26 @@ func newOpenTelemetryMeterProvider(ctx context.Context, config *types.OTLP) (*sd return nil, fmt.Errorf("creating exporter: %w", err) } + var resAttrs []attribute.KeyValue + for k, v := range config.ResourceAttributes { + resAttrs = append(resAttrs, attribute.String(k, v)) + } + res, err := resource.New(ctx, - resource.WithAttributes(semconv.ServiceNameKey.String(config.ServiceName)), - resource.WithAttributes(semconv.ServiceVersionKey.String(version.Version)), resource.WithContainer(), - resource.WithFromEnv(), resource.WithHost(), resource.WithOS(), resource.WithProcess(), resource.WithTelemetrySDK(), + // The following order allows the user to override the service name and version, + // as well as any other attributes set by the above detectors. + resource.WithAttributes( + semconv.ServiceName(config.ServiceName), + semconv.ServiceVersion(version.Version), + ), + resource.WithAttributes(resAttrs...), + // Use the environment variables to allow overriding above resource attributes. + resource.WithFromEnv(), ) if err != nil { return nil, fmt.Errorf("building resource: %w", err) diff --git a/pkg/types/logs.go b/pkg/types/logs.go index 9af55a8f0..a1af7f44c 100644 --- a/pkg/types/logs.go +++ b/pkg/types/logs.go @@ -150,7 +150,7 @@ func checkFieldHeaderValue(value, defaultValue string) string { // OTelLog provides configuration settings for the open-telemetry logger. type OTelLog struct { - ServiceName string `description:"Set the name for this service." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"` + ServiceName string `description:"Defines the service name resource attribute." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"` ResourceAttributes map[string]string `description:"Defines additional resource attributes (key:value)." json:"resourceAttributes,omitempty" toml:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty"` GRPC *OTelGRPC `description:"gRPC configuration for the OpenTelemetry collector." json:"grpc,omitempty" toml:"grpc,omitempty" yaml:"grpc,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` HTTP *OTelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` diff --git a/pkg/types/metrics.go b/pkg/types/metrics.go index 8ca8dec68..331544513 100644 --- a/pkg/types/metrics.go +++ b/pkg/types/metrics.go @@ -111,12 +111,13 @@ type OTLP 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"` HTTP *OTelHTTP `description:"HTTP configuration for the OpenTelemetry collector." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"` - AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"` - AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"` - ExplicitBoundaries []float64 `description:"Boundaries for latency metrics." json:"explicitBoundaries,omitempty" toml:"explicitBoundaries,omitempty" yaml:"explicitBoundaries,omitempty" export:"true"` - PushInterval types.Duration `description:"Period between calls to collect a checkpoint." json:"pushInterval,omitempty" toml:"pushInterval,omitempty" yaml:"pushInterval,omitempty" export:"true"` - ServiceName string `description:"OTEL service name to use." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"` + AddEntryPointsLabels bool `description:"Enable metrics on entry points." json:"addEntryPointsLabels,omitempty" toml:"addEntryPointsLabels,omitempty" yaml:"addEntryPointsLabels,omitempty" export:"true"` + AddRoutersLabels bool `description:"Enable metrics on routers." json:"addRoutersLabels,omitempty" toml:"addRoutersLabels,omitempty" yaml:"addRoutersLabels,omitempty" export:"true"` + AddServicesLabels bool `description:"Enable metrics on services." json:"addServicesLabels,omitempty" toml:"addServicesLabels,omitempty" yaml:"addServicesLabels,omitempty" export:"true"` + ExplicitBoundaries []float64 `description:"Boundaries for latency metrics." json:"explicitBoundaries,omitempty" toml:"explicitBoundaries,omitempty" yaml:"explicitBoundaries,omitempty" export:"true"` + PushInterval types.Duration `description:"Period between calls to collect a checkpoint." json:"pushInterval,omitempty" toml:"pushInterval,omitempty" yaml:"pushInterval,omitempty" export:"true"` + ServiceName string `description:"Defines the service name resource attribute." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"` + ResourceAttributes map[string]string `description:"Defines additional resource attributes (key:value)." json:"resourceAttributes,omitempty" toml:"resourceAttributes,omitempty" yaml:"resourceAttributes,omitempty" export:"true"` } // SetDefaults sets the default values. From 27326e6569d888224ec40a1aa719abdfc4eb44d7 Mon Sep 17 00:00:00 2001 From: Harold Ozouf Date: Fri, 18 Jul 2025 17:16:04 +0200 Subject: [PATCH 04/18] Redact logged install configuration --- cmd/traefik/traefik.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 9d3c7c953..b918c9f41 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -3,7 +3,6 @@ package main import ( "context" "crypto/x509" - "encoding/json" "fmt" stdlog "log" "net/http" @@ -34,6 +33,7 @@ import ( "github.com/traefik/traefik/v2/pkg/provider/acme" "github.com/traefik/traefik/v2/pkg/provider/aggregator" "github.com/traefik/traefik/v2/pkg/provider/traefik" + "github.com/traefik/traefik/v2/pkg/redactor" "github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/server" "github.com/traefik/traefik/v2/pkg/server/middleware" @@ -100,12 +100,11 @@ func runCmd(staticConfiguration *static.Configuration) error { log.WithoutContext().Infof("Traefik version %s built on %s", version.Version, version.BuildDate) - jsonConf, err := json.Marshal(staticConfiguration) + redactedStaticConfiguration, err := redactor.RemoveCredentials(staticConfiguration) if err != nil { - log.WithoutContext().Errorf("Could not marshal static configuration: %v", err) - log.WithoutContext().Debugf("Static configuration loaded [struct] %#v", staticConfiguration) + log.WithoutContext().Errorf("Could not redact static configuration: %v", err) } else { - log.WithoutContext().Debugf("Static configuration loaded %s", string(jsonConf)) + log.WithoutContext().Debugf("Static configuration loaded %s", redactedStaticConfiguration) } if staticConfiguration.Global.CheckNewVersion { From 78cc85283c41cc3592c44fd56909d8025b5f99f8 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Mon, 21 Jul 2025 12:06:04 +0200 Subject: [PATCH 05/18] Add k8s resource attributes automatically Co-authored-by: Romain --- cmd/traefik/logger.go | 11 +-- cmd/traefik/traefik.go | 19 ++--- docs/content/migration/v3.md | 23 +++++- .../kubernetes-crd-rbac.yml | 8 +++ .../kubernetes-gateway-rbac.yml | 8 +++ pkg/logs/otel.go | 5 +- pkg/logs/otel_test.go | 2 +- pkg/metrics/otel.go | 1 + pkg/middlewares/accesslog/logger.go | 4 +- pkg/middlewares/accesslog/logger_test.go | 10 +-- pkg/tracing/tracing.go | 13 +--- pkg/tracing/tracing_test.go | 4 +- pkg/types/k8sdetector.go | 70 +++++++++++++++++++ pkg/types/logs.go | 26 ++++--- pkg/types/tracing.go | 26 ++++--- 15 files changed, 170 insertions(+), 60 deletions(-) create mode 100644 pkg/types/k8sdetector.go diff --git a/cmd/traefik/logger.go b/cmd/traefik/logger.go index a8b609e4b..5e84e9118 100644 --- a/cmd/traefik/logger.go +++ b/cmd/traefik/logger.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "io" @@ -22,7 +23,7 @@ func init() { zerolog.SetGlobalLevel(zerolog.ErrorLevel) } -func setupLogger(staticConfiguration *static.Configuration) error { +func setupLogger(ctx context.Context, staticConfiguration *static.Configuration) error { // Validate that the experimental flag is set up at this point, // rather than validating the static configuration before the setupLogger call. // This ensures that validation messages are not logged using an un-configured logger. @@ -39,16 +40,16 @@ func setupLogger(staticConfiguration *static.Configuration) error { zerolog.SetGlobalLevel(logLevel) // create logger - logCtx := zerolog.New(w).With().Timestamp() + logger := zerolog.New(w).With().Timestamp() if logLevel <= zerolog.DebugLevel { - logCtx = logCtx.Caller() + logger = logger.Caller() } - log.Logger = logCtx.Logger().Level(logLevel) + log.Logger = logger.Logger().Level(logLevel) if staticConfiguration.Log != nil && staticConfiguration.Log.OTLP != nil { var err error - log.Logger, err = logs.SetupOTelLogger(log.Logger, staticConfiguration.Log.OTLP) + log.Logger, err = logs.SetupOTelLogger(ctx, log.Logger, staticConfiguration.Log.OTLP) if err != nil { return fmt.Errorf("setting up OpenTelemetry logger: %w", err) } diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 93e319f6a..dc80319e3 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -90,7 +90,10 @@ Complete documentation is available at https://traefik.io`, } func runCmd(staticConfiguration *static.Configuration) error { - if err := setupLogger(staticConfiguration); err != nil { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + if err := setupLogger(ctx, staticConfiguration); err != nil { return fmt.Errorf("setting up logger: %w", err) } @@ -123,8 +126,6 @@ func runCmd(staticConfiguration *static.Configuration) error { return err } - ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - if staticConfiguration.Ping != nil { staticConfiguration.Ping.WithContext(ctx) } @@ -210,8 +211,8 @@ func setupServer(staticConfiguration *static.Configuration) (*server.Server, err } } metricsRegistry := metrics.NewMultiRegistry(metricRegistries) - accessLog := setupAccessLog(staticConfiguration.AccessLog) - tracer, tracerCloser := setupTracing(staticConfiguration.Tracing) + accessLog := setupAccessLog(ctx, staticConfiguration.AccessLog) + tracer, tracerCloser := setupTracing(ctx, staticConfiguration.Tracing) observabilityMgr := middleware.NewObservabilityMgr(*staticConfiguration, metricsRegistry, semConvMetricRegistry, accessLog, tracer, tracerCloser) // Entrypoints @@ -586,12 +587,12 @@ func appendCertMetric(gauge gokitmetrics.Gauge, certificate *x509.Certificate) { gauge.With(labels...).Set(notAfter) } -func setupAccessLog(conf *types.AccessLog) *accesslog.Handler { +func setupAccessLog(ctx context.Context, conf *types.AccessLog) *accesslog.Handler { if conf == nil { return nil } - accessLoggerMiddleware, err := accesslog.NewHandler(conf) + accessLoggerMiddleware, err := accesslog.NewHandler(ctx, conf) if err != nil { log.Warn().Err(err).Msg("Unable to create access logger") return nil @@ -600,12 +601,12 @@ func setupAccessLog(conf *types.AccessLog) *accesslog.Handler { return accessLoggerMiddleware } -func setupTracing(conf *static.Tracing) (*tracing.Tracer, io.Closer) { +func setupTracing(ctx context.Context, conf *static.Tracing) (*tracing.Tracer, io.Closer) { if conf == nil { return nil, nil } - tracer, closer, err := tracing.NewTracing(conf) + tracer, closer, err := tracing.NewTracing(ctx, conf) if err != nil { log.Warn().Err(err).Msg("Unable to create tracer") return nil, nil diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md index e3cbe8a9b..7b676ee9e 100644 --- a/docs/content/migration/v3.md +++ b/docs/content/migration/v3.md @@ -322,9 +322,11 @@ and Traefik now keeps them encoded to avoid any ambiguity. ## v3.5.0 -### TraceVerbosity on Routers and Entrypoints +### Observability -Starting with v3.5, a new `traceVerbosity` option is available for both entrypoints and routers. +#### TraceVerbosity on Routers and Entrypoints + +Starting with `v3.5.0`, 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. @@ -339,3 +341,20 @@ Possible values are: - `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). + +#### K8s Resource Attributes + +Since `v3.5.0`, the semconv attributes `k8s.pod.name` and `k8s.pod.uid` are injected automatically in OTel resource attributes when OTel tracing/logs/metrics are enabled. + +For that purpose, the following right has to be added to the Traefik Kubernetes RBACs: + +```yaml + ... + - apiGroups: + - "" + resources: + - pods + verbs: + - get + ... +``` diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml index 87a438386..eb3f708bf 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-rbac.yml @@ -15,6 +15,14 @@ rules: - get - list - watch + # The pods right is needed to inject k8s.pod.uid and k8s.pod.name OTel attributes. + # When OTel tracing/logs/metrics are not enabled, this rule is not needed. + - apiGroups: + - "" + resources: + - pods + verbs: + - get - apiGroups: - discovery.k8s.io resources: diff --git a/docs/content/reference/dynamic-configuration/kubernetes-gateway-rbac.yml b/docs/content/reference/dynamic-configuration/kubernetes-gateway-rbac.yml index 31b0837c8..c03dc0147 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-gateway-rbac.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-gateway-rbac.yml @@ -11,6 +11,14 @@ rules: verbs: - list - watch + # The pods get right is needed to inject k8s.pod.uid and k8s.pod.name in OTel attributes. + # When OTel tracing/logs/metrics are not enabled, this rule is not needed. + - apiGroups: + - "" + resources: + - pods + verbs: + - get - apiGroups: - "" resources: diff --git a/pkg/logs/otel.go b/pkg/logs/otel.go index bc8f95443..f1e1711d5 100644 --- a/pkg/logs/otel.go +++ b/pkg/logs/otel.go @@ -1,6 +1,7 @@ package logs import ( + "context" "encoding/json" "fmt" "reflect" @@ -12,12 +13,12 @@ import ( ) // SetupOTelLogger sets up the OpenTelemetry logger. -func SetupOTelLogger(logger zerolog.Logger, config *types.OTelLog) (zerolog.Logger, error) { +func SetupOTelLogger(ctx context.Context, logger zerolog.Logger, config *types.OTelLog) (zerolog.Logger, error) { if config == nil { return logger, nil } - provider, err := config.NewLoggerProvider() + provider, err := config.NewLoggerProvider(ctx) if err != nil { return zerolog.Logger{}, fmt.Errorf("setting up OpenTelemetry logger provider: %w", err) } diff --git a/pkg/logs/otel_test.go b/pkg/logs/otel_test.go index c55137cf1..75f73be33 100644 --- a/pkg/logs/otel_test.go +++ b/pkg/logs/otel_test.go @@ -171,7 +171,7 @@ func TestLog(t *testing.T) { out := zerolog.MultiLevelWriter(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}) logger := zerolog.New(out).With().Caller().Logger() - logger, err := SetupOTelLogger(logger, config) + logger, err := SetupOTelLogger(t.Context(), logger, config) require.NoError(t, err) ctx := trace.ContextWithSpanContext(t.Context(), trace.NewSpanContext(trace.SpanContextConfig{ diff --git a/pkg/metrics/otel.go b/pkg/metrics/otel.go index b29a89f29..5da580bd8 100644 --- a/pkg/metrics/otel.go +++ b/pkg/metrics/otel.go @@ -217,6 +217,7 @@ func newOpenTelemetryMeterProvider(ctx context.Context, config *types.OTLP) (*sd resource.WithOS(), resource.WithProcess(), resource.WithTelemetrySDK(), + resource.WithDetectors(types.K8sAttributesDetector{}), // The following order allows the user to override the service name and version, // as well as any other attributes set by the above detectors. resource.WithAttributes( diff --git a/pkg/middlewares/accesslog/logger.go b/pkg/middlewares/accesslog/logger.go index 246556195..c9a3242df 100644 --- a/pkg/middlewares/accesslog/logger.go +++ b/pkg/middlewares/accesslog/logger.go @@ -85,7 +85,7 @@ func (h *Handler) AliceConstructor() alice.Constructor { } // NewHandler creates a new Handler. -func NewHandler(config *types.AccessLog) (*Handler, error) { +func NewHandler(ctx context.Context, config *types.AccessLog) (*Handler, error) { var file io.WriteCloser = noopCloser{os.Stdout} if len(config.FilePath) > 0 { f, err := openAccessLogFile(config.FilePath) @@ -116,7 +116,7 @@ func NewHandler(config *types.AccessLog) (*Handler, error) { } if config.OTLP != nil { - otelLoggerProvider, err := config.OTLP.NewLoggerProvider() + otelLoggerProvider, err := config.OTLP.NewLoggerProvider(ctx) if err != nil { return nil, fmt.Errorf("setting up OpenTelemetry logger provider: %w", err) } diff --git a/pkg/middlewares/accesslog/logger_test.go b/pkg/middlewares/accesslog/logger_test.go index 7babf8121..fbf9fd380 100644 --- a/pkg/middlewares/accesslog/logger_test.go +++ b/pkg/middlewares/accesslog/logger_test.go @@ -85,7 +85,7 @@ func TestOTelAccessLog(t *testing.T) { }, }, } - logHandler, err := NewHandler(config) + logHandler, err := NewHandler(t.Context(), config) require.NoError(t, err) t.Cleanup(func() { err := logHandler.Close() @@ -138,7 +138,7 @@ func TestLogRotation(t *testing.T) { rotatedFileName := fileName + ".rotated" config := &types.AccessLog{FilePath: fileName, Format: CommonFormat} - logHandler, err := NewHandler(config) + logHandler, err := NewHandler(t.Context(), config) require.NoError(t, err) t.Cleanup(func() { err := logHandler.Close() @@ -282,7 +282,7 @@ func TestLoggerHeaderFields(t *testing.T) { Fields: &test.accessLogFields, } - logger, err := NewHandler(config) + logger, err := NewHandler(t.Context(), config) require.NoError(t, err) t.Cleanup(func() { err := logger.Close() @@ -979,7 +979,7 @@ func captureStdout(t *testing.T) (out *os.File, restoreStdout func()) { func doLoggingTLSOpt(t *testing.T, config *types.AccessLog, enableTLS, tracing bool) { t.Helper() - logger, err := NewHandler(config) + logger, err := NewHandler(t.Context(), config) require.NoError(t, err) t.Cleanup(func() { err := logger.Close() @@ -1076,7 +1076,7 @@ func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) { func doLoggingWithAbortedStream(t *testing.T, config *types.AccessLog) { t.Helper() - logger, err := NewHandler(config) + logger, err := NewHandler(t.Context(), config) require.NoError(t, err) t.Cleanup(func() { err := logger.Close() diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index 76a3d3302..c2f998eae 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -25,11 +25,11 @@ import ( // Backend is an abstraction for tracking backend (OpenTelemetry, ...). type Backend interface { - Setup(serviceName string, sampleRate float64, resourceAttributes map[string]string) (trace.Tracer, io.Closer, error) + Setup(ctx context.Context, serviceName string, sampleRate float64, resourceAttributes map[string]string) (trace.Tracer, io.Closer, error) } // NewTracing Creates a Tracing. -func NewTracing(conf *static.Tracing) (*Tracer, io.Closer, error) { +func NewTracing(ctx context.Context, conf *static.Tracing) (*Tracer, io.Closer, error) { var backend Backend if conf.OTLP != nil { @@ -44,7 +44,7 @@ func NewTracing(conf *static.Tracing) (*Tracer, io.Closer, error) { otel.SetTextMapPropagator(autoprop.NewTextMapPropagator()) - tr, closer, err := backend.Setup(conf.ServiceName, conf.SampleRate, conf.ResourceAttributes) + tr, closer, err := backend.Setup(ctx, conf.ServiceName, conf.SampleRate, conf.ResourceAttributes) if err != nil { return nil, nil, err } @@ -84,13 +84,6 @@ func InjectContextIntoCarrier(req *http.Request) { propagator.Inject(req.Context(), propagation.HeaderCarrier(req.Header)) } -// SetStatusErrorf flags the span as in error and log an event. -func SetStatusErrorf(ctx context.Context, format string, args ...interface{}) { - if span := trace.SpanFromContext(ctx); span != nil { - span.SetStatus(codes.Error, fmt.Sprintf(format, args...)) - } -} - // Span is trace.Span wrapping the Traefik TracerProvider. type Span struct { trace.Span diff --git a/pkg/tracing/tracing_test.go b/pkg/tracing/tracing_test.go index 9507b528c..e3cc39cea 100644 --- a/pkg/tracing/tracing_test.go +++ b/pkg/tracing/tracing_test.go @@ -350,7 +350,7 @@ func TestTracing(t *testing.T) { }, } - tracer, closer, err := NewTracing(tracingConfig) + tracer, closer, err := NewTracing(t.Context(), tracingConfig) require.NoError(t, err) t.Cleanup(func() { _ = closer.Close() @@ -402,7 +402,7 @@ func TestTracerProvider(t *testing.T) { otlpConfig.SetDefaults() config := &static.Tracing{OTLP: otlpConfig} - tracer, closer, err := NewTracing(config) + tracer, closer, err := NewTracing(t.Context(), config) if err != nil { t.Fatal(err) } diff --git a/pkg/types/k8sdetector.go b/pkg/types/k8sdetector.go new file mode 100644 index 000000000..d87589c1f --- /dev/null +++ b/pkg/types/k8sdetector.go @@ -0,0 +1,70 @@ +package types + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + kerror "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kclientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// K8sAttributesDetector detects the metadata of the Traefik pod running in a Kubernetes cluster. +// It reads the pod name from the hostname file and the namespace from the service account namespace file and queries the Kubernetes API to get the pod's UID. +type K8sAttributesDetector struct{} + +func (K8sAttributesDetector) Detect(ctx context.Context) (*resource.Resource, error) { + attrs := os.Getenv("OTEL_RESOURCE_ATTRIBUTES") + if strings.Contains(attrs, string(semconv.K8SPodNameKey)) || strings.Contains(attrs, string(semconv.K8SPodUIDKey)) { + return resource.Empty(), nil + } + + // The InClusterConfig function returns a config for the Kubernetes API server + // when it is running inside a Kubernetes cluster. + config, err := rest.InClusterConfig() + if err != nil && errors.Is(err, rest.ErrNotInCluster) { + return resource.Empty(), nil + } + if err != nil { + return nil, fmt.Errorf("creating in cluster config: %w", err) + } + + client, err := kclientset.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("creating Kubernetes client: %w", err) + } + + podName, err := os.Hostname() + if err != nil { + return nil, fmt.Errorf("getting pod name: %w", err) + } + + podNamespaceBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return nil, fmt.Errorf("getting pod namespace: %w", err) + } + podNamespace := string(podNamespaceBytes) + + pod, err := client.CoreV1().Pods(podNamespace).Get(ctx, podName, metav1.GetOptions{}) + if err != nil && kerror.IsForbidden(err) { + log.Error().Err(err).Msg("Unable to build K8s resource attributes for Traefik pod") + return resource.Empty(), nil + } + if err != nil { + return nil, fmt.Errorf("getting pod metadata: %w", err) + } + + // To avoid version conflicts with other detectors, we use a Schemaless resource. + return resource.NewSchemaless( + semconv.K8SPodUID(string(pod.UID)), + semconv.K8SPodName(pod.Name), + semconv.K8SNamespaceName(podNamespace), + ), nil +} diff --git a/pkg/types/logs.go b/pkg/types/logs.go index a1af7f44c..c476d166d 100644 --- a/pkg/types/logs.go +++ b/pkg/types/logs.go @@ -13,7 +13,7 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" otelsdk "go.opentelemetry.io/otel/sdk/log" "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "google.golang.org/grpc/credentials" "google.golang.org/grpc/encoding/gzip" ) @@ -164,7 +164,7 @@ func (o *OTelLog) SetDefaults() { } // NewLoggerProvider creates a new OpenTelemetry logger provider. -func (o *OTelLog) NewLoggerProvider() (*otelsdk.LoggerProvider, error) { +func (o *OTelLog) NewLoggerProvider(ctx context.Context) (*otelsdk.LoggerProvider, error) { var ( err error exporter otelsdk.Exporter @@ -178,23 +178,27 @@ func (o *OTelLog) NewLoggerProvider() (*otelsdk.LoggerProvider, error) { return nil, fmt.Errorf("setting up exporter: %w", err) } - attr := []attribute.KeyValue{ - semconv.ServiceNameKey.String(o.ServiceName), - semconv.ServiceVersionKey.String(version.Version), - } - + var resAttrs []attribute.KeyValue for k, v := range o.ResourceAttributes { - attr = append(attr, attribute.String(k, v)) + resAttrs = append(resAttrs, attribute.String(k, v)) } - res, err := resource.New(context.Background(), - resource.WithAttributes(attr...), + res, err := resource.New(ctx, resource.WithContainer(), - resource.WithFromEnv(), resource.WithHost(), resource.WithOS(), resource.WithProcess(), resource.WithTelemetrySDK(), + resource.WithDetectors(K8sAttributesDetector{}), + // The following order allows the user to override the service name and version, + // as well as any other attributes set by the above detectors. + resource.WithAttributes( + semconv.ServiceName(o.ServiceName), + semconv.ServiceVersion(version.Version), + ), + resource.WithAttributes(resAttrs...), + // Use the environment variables to allow overriding above resource attributes. + resource.WithFromEnv(), ) if err != nil { return nil, fmt.Errorf("building resource: %w", err) diff --git a/pkg/types/tracing.go b/pkg/types/tracing.go index 0ee66ea3c..d07bb43c5 100644 --- a/pkg/types/tracing.go +++ b/pkg/types/tracing.go @@ -17,7 +17,7 @@ import ( "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/credentials" "google.golang.org/grpc/encoding/gzip" @@ -52,7 +52,7 @@ func (c *OTelTracing) SetDefaults() { } // Setup sets up the tracer. -func (c *OTelTracing) Setup(serviceName string, sampleRate float64, resourceAttributes map[string]string) (trace.Tracer, io.Closer, error) { +func (c *OTelTracing) Setup(ctx context.Context, serviceName string, sampleRate float64, resourceAttributes map[string]string) (trace.Tracer, io.Closer, error) { var ( err error exporter *otlptrace.Exporter @@ -66,23 +66,27 @@ func (c *OTelTracing) Setup(serviceName string, sampleRate float64, resourceAttr return nil, nil, fmt.Errorf("setting up exporter: %w", err) } - attr := []attribute.KeyValue{ - semconv.ServiceNameKey.String(serviceName), - semconv.ServiceVersionKey.String(version.Version), - } - + var resAttrs []attribute.KeyValue for k, v := range resourceAttributes { - attr = append(attr, attribute.String(k, v)) + resAttrs = append(resAttrs, attribute.String(k, v)) } - res, err := resource.New(context.Background(), - resource.WithAttributes(attr...), + res, err := resource.New(ctx, resource.WithContainer(), - resource.WithFromEnv(), resource.WithHost(), resource.WithOS(), resource.WithProcess(), resource.WithTelemetrySDK(), + resource.WithDetectors(K8sAttributesDetector{}), + // The following order allows the user to override the service name and version, + // as well as any other attributes set by the above detectors. + resource.WithAttributes( + semconv.ServiceName(serviceName), + semconv.ServiceVersion(version.Version), + ), + resource.WithAttributes(resAttrs...), + // Use the environment variables to allow overriding above resource attributes. + resource.WithFromEnv(), ) if err != nil { return nil, nil, fmt.Errorf("building resource: %w", err) From b2b4b66b08e6386a4e4251dc974da5fef73c6d54 Mon Sep 17 00:00:00 2001 From: Romain Date: Tue, 22 Jul 2025 11:10:05 +0200 Subject: [PATCH 06/18] Disable MPTCP by default Co-authored-by: Kevin Pollet --- docs/content/migration/v2.md | 11 +++++++++++ pkg/server/server_entrypoint_tcp.go | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index a2106a5cd..70261ead3 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -703,3 +703,14 @@ 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 | + +## v2.11.28 + +### MultiPath TCP + +Since `v2.11.28`, the MultiPath TCP support introduced with `v2.11.26` has been removed. +It appears that enabling MPTCP on some platforms can cause Traefik to stop with the following error logs message: + +- `set tcp X.X.X.X:X->X.X.X.X:X: setsockopt: operation not supported` + +However, it can be re-enabled by setting the `multipathtcp` variable in the GODEBUG environment variable, see the related [go documentation](https://go.dev/doc/godebug#go-124). diff --git a/pkg/server/server_entrypoint_tcp.go b/pkg/server/server_entrypoint_tcp.go index 3257d6720..1036240cf 100644 --- a/pkg/server/server_entrypoint_tcp.go +++ b/pkg/server/server_entrypoint_tcp.go @@ -457,7 +457,15 @@ func buildProxyProtocolListener(ctx context.Context, entryPoint *static.EntryPoi } func buildListener(ctx context.Context, entryPoint *static.EntryPoint) (net.Listener, error) { - listener, err := net.Listen("tcp", entryPoint.GetAddress()) + config := net.ListenConfig{} + + // TODO: Look into configuring keepAlive period through listenConfig instead of our custom tcpKeepAliveListener, to reactivate MultipathTCP? + // MultipathTCP is not supported on all platforms, and is notably unsupported in combination with TCP keep-alive. + if !strings.Contains(os.Getenv("GODEBUG"), "multipathtcp") { + config.SetMultipathTCP(false) + } + + listener, err := config.Listen(ctx, "tcp", entryPoint.GetAddress()) if err != nil { return nil, fmt.Errorf("error opening listener: %w", err) } From 5ef853a0c53068f69a6c229a5815a0dc6e0a8800 Mon Sep 17 00:00:00 2001 From: Zeroday BYTE Date: Tue, 22 Jul 2025 05:24:05 -0700 Subject: [PATCH 07/18] Fix client arbitrary file access during archive extraction zipslip --- pkg/plugins/client.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pkg/plugins/client.go b/pkg/plugins/client.go index 642b2dc68..1cee55d3f 100644 --- a/pkg/plugins/client.go +++ b/pkg/plugins/client.go @@ -240,6 +240,8 @@ func (c *Client) Unzip(pName, pVersion string) error { return nil } + // Unzip as a generic archive if the module unzip fails. + // This is useful for plugins that have vendor directories or other structures. return c.unzipArchive(pName, pVersion) } @@ -280,24 +282,48 @@ func unzipFile(f *zipa.File, dest string) error { defer func() { _ = rc.Close() }() + // Split to discard the first part of the path, which is [organization]-[project]-[release commit sha1] when the archive is a Yaegi go plugin with vendoring. pathParts := strings.SplitN(f.Name, "/", 2) - p := filepath.Join(dest, pathParts[1]) + if len(pathParts) < 2 { + return fmt.Errorf("no root directory: %s", f.Name) + } + + // Validate and sanitize the file path. + cleanName := filepath.Clean(pathParts[1]) + if strings.Contains(cleanName, "..") { + return fmt.Errorf("invalid file path in archive: %s", f.Name) + } + + filePath := filepath.Join(dest, cleanName) + absFilePath, err := filepath.Abs(filePath) + if err != nil { + return fmt.Errorf("resolving file path: %w", err) + } + + absDest, err := filepath.Abs(dest) + if err != nil { + return fmt.Errorf("resolving destination directory: %w", err) + } + + if !strings.HasPrefix(absFilePath, absDest) { + return fmt.Errorf("file path escapes destination directory: %s", absFilePath) + } if f.FileInfo().IsDir() { - err = os.MkdirAll(p, f.Mode()) + err = os.MkdirAll(filePath, f.Mode()) if err != nil { - return fmt.Errorf("unable to create archive directory %s: %w", p, err) + return fmt.Errorf("unable to create archive directory %s: %w", filePath, err) } return nil } - err = os.MkdirAll(filepath.Dir(p), 0o750) + err = os.MkdirAll(filepath.Dir(filePath), 0o750) if err != nil { - return fmt.Errorf("unable to create archive directory %s for file %s: %w", filepath.Dir(p), p, err) + return fmt.Errorf("unable to create archive directory %s for file %s: %w", filepath.Dir(filePath), filePath, err) } - elt, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + elt, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } From 96386b1d789fda01a4c5d24aac18a86f0b5bec26 Mon Sep 17 00:00:00 2001 From: GreyXor <79602273+GreyXor@users.noreply.github.com> Date: Tue, 22 Jul 2025 14:54:04 +0200 Subject: [PATCH 08/18] Bump github.com/quic-go/quic-go to v0.54.0 --- go.mod | 3 +-- go.sum | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c280c5800..f670bd343 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo. github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_model v0.6.1 - github.com/quic-go/quic-go v0.49.0 + github.com/quic-go/quic-go v0.54.0 github.com/redis/go-redis/v9 v9.7.3 github.com/rs/zerolog v1.33.0 github.com/sirupsen/logrus v1.9.3 @@ -205,7 +205,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.16.0 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect - github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect github.com/go-zookeeper/zk v1.0.3 // indirect github.com/goccy/go-json v0.10.5 // indirect diff --git a/go.sum b/go.sum index 9a9808eba..f1977e913 100644 --- a/go.sum +++ b/go.sum @@ -423,6 +423,7 @@ github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptd github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= @@ -1035,8 +1036,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.49.0 h1:w5iJHXwHxs1QxyBv1EHKuC50GX5to8mJAxvtnttJp94= -github.com/quic-go/quic-go v0.49.0/go.mod h1:s2wDnmCdooUQBmQfpUSTCYBl1/D4FcqbULMMkASvR6s= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= From 50931813f2eeb178e78a641e6aedd314fa000043 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:44:05 +0200 Subject: [PATCH 09/18] Remove all mentions of ordering for TLSOption CurvePreferences field --- docs/content/https/tls.md | 4 ++-- .../dynamic-configuration/kubernetes-crd-definition-v1.yml | 4 ++-- .../dynamic-configuration/traefik.containo.us_tlsoptions.yaml | 2 +- .../dynamic-configuration/traefik.io_tlsoptions.yaml | 2 +- docs/content/routing/providers/kubernetes-crd.md | 2 +- integration/fixtures/k8s/01-traefik-crd.yml | 4 ++-- .../kubernetes/crd/traefikcontainous/v1alpha1/tlsoption.go | 2 +- pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/content/https/tls.md b/docs/content/https/tls.md index 0323eb2e6..505043259 100644 --- a/docs/content/https/tls.md +++ b/docs/content/https/tls.md @@ -392,11 +392,11 @@ spec: ### Curve Preferences -This option allows to set the preferred elliptic curves in a specific order. +This option allows to set the enabled elliptic curves for key exchange. The names of the curves defined by [`crypto`](https://godoc.org/crypto/tls#CurveID) (e.g. `CurveP521`) and the [RFC defined names](https://tools.ietf.org/html/rfc8446#section-4.2.7) (e. g. `secp521r1`) can be used. -See [CurveID](https://godoc.org/crypto/tls#CurveID) for more information. +See [CurvePreferences](https://godoc.org/crypto/tls#Config.CurvePreferences) and [CurveID](https://godoc.org/crypto/tls#CurveID) for more information. ```yaml tab="File (YAML)" # Dynamic configuration diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 6f901cb57..b5c41bb8c 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1886,7 +1886,7 @@ spec: type: object curvePreferences: description: |- - CurvePreferences defines the preferred elliptic curves in a specific order. + CurvePreferences defines the preferred elliptic curves. More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences items: type: string @@ -4316,7 +4316,7 @@ spec: type: object curvePreferences: description: |- - CurvePreferences defines the preferred elliptic curves in a specific order. + CurvePreferences defines the preferred elliptic curves. More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences items: type: string diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml index daa25640d..6c7fdc914 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_tlsoptions.yaml @@ -78,7 +78,7 @@ spec: type: object curvePreferences: description: |- - CurvePreferences defines the preferred elliptic curves in a specific order. + CurvePreferences defines the preferred elliptic curves. More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences items: type: string diff --git a/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml b/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml index 0fdd05bc4..20f817125 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_tlsoptions.yaml @@ -78,7 +78,7 @@ spec: type: object curvePreferences: description: |- - CurvePreferences defines the preferred elliptic curves in a specific order. + CurvePreferences defines the preferred elliptic curves. More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences items: type: string diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index 20e67ce2a..cf168ff91 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -1644,7 +1644,7 @@ or referencing TLS options in the [`IngressRoute`](#kind-ingressroute) / [`Ingre | [2] | `minVersion` | Defines the [minimum TLS version](../../https/tls.md#minimum-tls-version) that is acceptable. | | [3] | `maxVersion` | Defines the [maximum TLS version](../../https/tls.md#maximum-tls-version) that is acceptable. | | [4] | `cipherSuites` | list of supported [cipher suites](../../https/tls.md#cipher-suites) for TLS versions up to TLS 1.2. | -| [5] | `curvePreferences` | List of the [elliptic curves references](../../https/tls.md#curve-preferences) that will be used in an ECDHE handshake, in preference order. | +| [5] | `curvePreferences` | List of the [elliptic curves references](../../https/tls.md#curve-preferences) that will be used in an ECDHE handshake. | | [6] | `clientAuth` | determines the server's policy for TLS [Client Authentication](../../https/tls.md#client-authentication-mtls). | | [7] | `clientAuth.secretNames` | list of names of the referenced Kubernetes [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) (in TLSOption namespace). The secret must contain a certificate under either a `tls.ca` or a `ca.crt` key. | | [8] | `clientAuth.clientAuthType` | defines the client authentication type to apply. The available values are: `NoClientCert`, `RequestClientCert`, `VerifyClientCertIfGiven` and `RequireAndVerifyClientCert`. | diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 6f901cb57..b5c41bb8c 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1886,7 +1886,7 @@ spec: type: object curvePreferences: description: |- - CurvePreferences defines the preferred elliptic curves in a specific order. + CurvePreferences defines the preferred elliptic curves. More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences items: type: string @@ -4316,7 +4316,7 @@ spec: type: object curvePreferences: description: |- - CurvePreferences defines the preferred elliptic curves in a specific order. + CurvePreferences defines the preferred elliptic curves. More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences items: type: string diff --git a/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1/tlsoption.go b/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1/tlsoption.go index 0e6e2274d..74aba1a91 100644 --- a/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1/tlsoption.go +++ b/pkg/provider/kubernetes/crd/traefikcontainous/v1alpha1/tlsoption.go @@ -34,7 +34,7 @@ type TLSOptionSpec struct { // CipherSuites defines the list of supported cipher suites for TLS versions up to TLS 1.2. // More info: https://doc.traefik.io/traefik/v2.11/https/tls/#cipher-suites CipherSuites []string `json:"cipherSuites,omitempty"` - // CurvePreferences defines the preferred elliptic curves in a specific order. + // CurvePreferences defines the preferred elliptic curves. // More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences CurvePreferences []string `json:"curvePreferences,omitempty"` // ClientAuth defines the server's policy for TLS Client Authentication. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go index 0e6e2274d..74aba1a91 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/tlsoption.go @@ -34,7 +34,7 @@ type TLSOptionSpec struct { // CipherSuites defines the list of supported cipher suites for TLS versions up to TLS 1.2. // More info: https://doc.traefik.io/traefik/v2.11/https/tls/#cipher-suites CipherSuites []string `json:"cipherSuites,omitempty"` - // CurvePreferences defines the preferred elliptic curves in a specific order. + // CurvePreferences defines the preferred elliptic curves. // More info: https://doc.traefik.io/traefik/v2.11/https/tls/#curve-preferences CurvePreferences []string `json:"curvePreferences,omitempty"` // ClientAuth defines the server's policy for TLS Client Authentication. From 028e8ca0b02bdd72f2cd1dab3755053472f17bd9 Mon Sep 17 00:00:00 2001 From: Landry Benguigui Date: Tue, 22 Jul 2025 17:10:05 +0200 Subject: [PATCH 10/18] Revert 11711 adding url param to healthcheck command --- cmd/healthcheck/healthcheck.go | 22 +++---------------- .../observability/healthcheck.md | 10 --------- 2 files changed, 3 insertions(+), 29 deletions(-) diff --git a/cmd/healthcheck/healthcheck.go b/cmd/healthcheck/healthcheck.go index dd83cb159..a3b720eb6 100644 --- a/cmd/healthcheck/healthcheck.go +++ b/cmd/healthcheck/healthcheck.go @@ -2,7 +2,6 @@ package healthcheck import ( "errors" - "flag" "fmt" "net/http" "os" @@ -23,26 +22,11 @@ func NewCmd(traefikConfiguration *static.Configuration, loaders []cli.ResourceLo } } -func runCmd(traefikConfiguration *static.Configuration) func(args []string) error { - return func(args []string) error { - fs := flag.NewFlagSet("healthcheck", flag.ContinueOnError) - urlFlag := fs.String("url", "", "") - fs.SetOutput(os.Stderr) - if err := fs.Parse(args); err != nil { - return err - } - +func runCmd(traefikConfiguration *static.Configuration) func(_ []string) error { + return func(_ []string) error { traefikConfiguration.SetEffectiveConfiguration() - var resp *http.Response - var errPing error - if *urlFlag != "" { - client := &http.Client{Timeout: 5 * time.Second} - resp, errPing = client.Head(*urlFlag) - } else { - resp, errPing = Do(*traefikConfiguration) - } - + resp, errPing := Do(*traefikConfiguration) if resp != nil { resp.Body.Close() } diff --git a/docs/content/reference/install-configuration/observability/healthcheck.md b/docs/content/reference/install-configuration/observability/healthcheck.md index 9282b6b79..742480286 100644 --- a/docs/content/reference/install-configuration/observability/healthcheck.md +++ b/docs/content/reference/install-configuration/observability/healthcheck.md @@ -27,16 +27,6 @@ $ traefik healthcheck OK: http://:8082/ping ``` -### URL Option - -The URL to check can be specified with the `--url` flag, which defaults to `http://localhost:8080/ping`. - -Example: - -```sh -traefik healthcheck --url=http://localhost:8080/ping -``` - ## Ping The `/ping` health-check URL is enabled with the command-line `--ping` or config file option `[ping]`. From 117e0b44718687f74482ff12553e71d2bbbd7e1c Mon Sep 17 00:00:00 2001 From: Nicolas Mengin Date: Wed, 23 Jul 2025 09:30:05 +0200 Subject: [PATCH 11/18] Add extended NGinX annotation support documentation --- .../kubernetes/ingress-nginx.md | 194 +++++++++--------- 1 file changed, 101 insertions(+), 93 deletions(-) diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index 5d12b0237..22a9c04d5 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -296,99 +296,107 @@ Limitations or behavioral differences are indicated where relevant. ### Unsupported NGINX Annotations -All other NGINX annotations not listed above, including but not limited to: +!!! question "Want to Add Support for More Annotations?" + + You can help extend support in two ways: + + - [**Open a PR**](../../../contributing/submitting-pull-requests.md) with the new annotation support. + - **Reach out** to the [Traefik Labs support team](https://info.traefik.io/request-commercial-support?cta=doc). + + All contributions and suggestions are welcome — let's build this together! + | Annotation | Notes | |-----------------------------------------------------------------------------|------------------------------------------------------| -| `nginx.ingress.kubernetes.io/app-root` | Not supported. | -| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-tls-secret` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-tls-verify-depth` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-tls-verify-client` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-tls-error-page` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-tls-match-cn` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-cache-key` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-cache-duration` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-keepalive` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-keepalive-share-vars` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-keepalive-requests` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-keepalive-timeout` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-proxy-set-headers` | Not supported. | -| `nginx.ingress.kubernetes.io/auth-snippet` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-global-auth` | Not supported. | -| `nginx.ingress.kubernetes.io/canary` | Not supported. | -| `nginx.ingress.kubernetes.io/canary-by-header` | Not supported. | -| `nginx.ingress.kubernetes.io/canary-by-header-value` | Not supported. | -| `nginx.ingress.kubernetes.io/canary-by-header-pattern` | Not supported. | -| `nginx.ingress.kubernetes.io/canary-by-cookie` | Not supported. | -| `nginx.ingress.kubernetes.io/canary-weight` | Not supported. | -| `nginx.ingress.kubernetes.io/canary-weight-total` | Not supported. | -| `nginx.ingress.kubernetes.io/client-body-buffer-size` | Not supported. | -| `nginx.ingress.kubernetes.io/configuration-snippet` | Not supported. | -| `nginx.ingress.kubernetes.io/custom-http-errors` | Not supported. | -| `nginx.ingress.kubernetes.io/disable-proxy-intercept-errors` | Not supported. | -| `nginx.ingress.kubernetes.io/default-backend` | Not supported; use `defaultBackend` in Ingress spec. | -| `nginx.ingress.kubernetes.io/limit-rate-after` | Not supported. | -| `nginx.ingress.kubernetes.io/limit-rate` | Not supported. | -| `nginx.ingress.kubernetes.io/limit-whitelist` | Not supported. | -| `nginx.ingress.kubernetes.io/limit-rps` | Not supported. | -| `nginx.ingress.kubernetes.io/limit-rpm` | Not supported. | -| `nginx.ingress.kubernetes.io/limit-burst-multiplier` | Not supported. | -| `nginx.ingress.kubernetes.io/limit-connections` | Not supported. | -| `nginx.ingress.kubernetes.io/global-rate-limit` | Not supported. | -| `nginx.ingress.kubernetes.io/global-rate-limit-window` | Not supported. | -| `nginx.ingress.kubernetes.io/global-rate-limit-key` | Not supported. | -| `nginx.ingress.kubernetes.io/global-rate-limit-ignored-cidrs` | Not supported. | -| `nginx.ingress.kubernetes.io/permanent-redirect` | Not supported. | -| `nginx.ingress.kubernetes.io/permanent-redirect-code` | Not supported. | -| `nginx.ingress.kubernetes.io/temporal-redirect` | Not supported. | -| `nginx.ingress.kubernetes.io/preserve-trailing-slash` | Not supported; Traefik preserves by default. | -| `nginx.ingress.kubernetes.io/proxy-cookie-domain` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-cookie-path` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-connect-timeout` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-send-timeout` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-read-timeout` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-next-upstream` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-next-upstream-timeout` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-next-upstream-tries` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-request-buffering` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-redirect-from` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-redirect-to` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-http-version` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-ssl-ciphers` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-ssl-verify-depth` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-ssl-protocols` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-rewrite-log` | Not supported. | -| `nginx.ingress.kubernetes.io/rewrite-target` | Not supported. | -| `nginx.ingress.kubernetes.io/satisfy` | Not supported. | -| `nginx.ingress.kubernetes.io/server-alias` | Not supported. | -| `nginx.ingress.kubernetes.io/server-snippet` | Not supported. | -| `nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none` | Not supported. | -| `nginx.ingress.kubernetes.io/session-cookie-expires` | Not supported. | -| `nginx.ingress.kubernetes.io/session-cookie-change-on-failure` | Not supported. | -| `nginx.ingress.kubernetes.io/ssl-ciphers` | Not supported. | -| `nginx.ingress.kubernetes.io/ssl-prefer-server-ciphers` | Not supported. | -| `nginx.ingress.kubernetes.io/connection-proxy-header` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-access-log` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-opentracing` | Not supported. | -| `nginx.ingress.kubernetes.io/opentracing-trust-incoming-span` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-opentelemetry` | Not supported. | -| `nginx.ingress.kubernetes.io/opentelemetry-trust-incoming-span` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-modsecurity` | Not supported. | -| `nginx.ingress.kubernetes.io/enable-owasp-core-rules` | Not supported. | -| `nginx.ingress.kubernetes.io/modsecurity-transaction-id` | Not supported. | -| `nginx.ingress.kubernetes.io/modsecurity-snippet` | Not supported. | -| `nginx.ingress.kubernetes.io/mirror-request-body` | Not supported. | -| `nginx.ingress.kubernetes.io/mirror-target` | Not supported. | -| `nginx.ingress.kubernetes.io/mirror-host` | Not supported. | -| `nginx.ingress.kubernetes.io/x-forwarded-prefix` | Not supported. | -| `nginx.ingress.kubernetes.io/upstream-hash-by` | Not supported. | -| `nginx.ingress.kubernetes.io/upstream-vhost` | Not supported. | -| `nginx.ingress.kubernetes.io/denylist-source-range` | Not supported. | -| `nginx.ingress.kubernetes.io/whitelist-source-range` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-buffering` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-buffers-number` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-buffer-size` | Not supported. | -| `nginx.ingress.kubernetes.io/proxy-max-temp-file-size` | Not supported. | -| `nginx.ingress.kubernetes.io/stream-snippet` | Not supported. | +| `nginx.ingress.kubernetes.io/app-root` | Not supported yet. | +| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-tls-secret` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-tls-verify-depth` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-tls-verify-client` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-tls-error-page` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-tls-match-cn` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-cache-key` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-cache-duration` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-keepalive` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-keepalive-share-vars` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-keepalive-requests` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-keepalive-timeout` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-proxy-set-headers` | Not supported yet. | +| `nginx.ingress.kubernetes.io/auth-snippet` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-global-auth` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary-by-header` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary-by-header-value` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary-by-header-pattern` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary-by-cookie` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary-weight` | Not supported yet. | +| `nginx.ingress.kubernetes.io/canary-weight-total` | Not supported yet. | +| `nginx.ingress.kubernetes.io/client-body-buffer-size` | Not supported yet. | +| `nginx.ingress.kubernetes.io/configuration-snippet` | Not supported yet. | +| `nginx.ingress.kubernetes.io/custom-http-errors` | Not supported yet. | +| `nginx.ingress.kubernetes.io/disable-proxy-intercept-errors` | Not supported yet. | +| `nginx.ingress.kubernetes.io/default-backend` | Not supported yet; use `defaultBackend` in Ingress spec. | +| `nginx.ingress.kubernetes.io/limit-rate-after` | Not supported yet. | +| `nginx.ingress.kubernetes.io/limit-rate` | Not supported yet. | +| `nginx.ingress.kubernetes.io/limit-whitelist` | Not supported yet. | +| `nginx.ingress.kubernetes.io/limit-rps` | Not supported yet. | +| `nginx.ingress.kubernetes.io/limit-rpm` | Not supported yet. | +| `nginx.ingress.kubernetes.io/limit-burst-multiplier` | Not supported yet. | +| `nginx.ingress.kubernetes.io/limit-connections` | Not supported yet. | +| `nginx.ingress.kubernetes.io/global-rate-limit` | Not supported yet. | +| `nginx.ingress.kubernetes.io/global-rate-limit-window` | Not supported yet. | +| `nginx.ingress.kubernetes.io/global-rate-limit-key` | Not supported yet. | +| `nginx.ingress.kubernetes.io/global-rate-limit-ignored-cidrs` | Not supported yet. | +| `nginx.ingress.kubernetes.io/permanent-redirect` | Not supported yet. | +| `nginx.ingress.kubernetes.io/permanent-redirect-code` | Not supported yet. | +| `nginx.ingress.kubernetes.io/temporal-redirect` | Not supported yet. | +| `nginx.ingress.kubernetes.io/preserve-trailing-slash` | Not supported yet; Traefik preserves by default. | +| `nginx.ingress.kubernetes.io/proxy-cookie-domain` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-cookie-path` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-connect-timeout` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-send-timeout` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-read-timeout` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-next-upstream` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-next-upstream-timeout` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-next-upstream-tries` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-request-buffering` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-redirect-from` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-redirect-to` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-http-version` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-ssl-ciphers` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-ssl-verify-depth` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-ssl-protocols` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-rewrite-log` | Not supported yet. | +| `nginx.ingress.kubernetes.io/rewrite-target` | Not supported yet. | +| `nginx.ingress.kubernetes.io/satisfy` | Not supported yet. | +| `nginx.ingress.kubernetes.io/server-alias` | Not supported yet. | +| `nginx.ingress.kubernetes.io/server-snippet` | Not supported yet. | +| `nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none` | Not supported yet. | +| `nginx.ingress.kubernetes.io/session-cookie-expires` | Not supported yet. | +| `nginx.ingress.kubernetes.io/session-cookie-change-on-failure` | Not supported yet. | +| `nginx.ingress.kubernetes.io/ssl-ciphers` | Not supported yet. | +| `nginx.ingress.kubernetes.io/ssl-prefer-server-ciphers` | Not supported yet. | +| `nginx.ingress.kubernetes.io/connection-proxy-header` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-access-log` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-opentracing` | Not supported yet. | +| `nginx.ingress.kubernetes.io/opentracing-trust-incoming-span` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-opentelemetry` | Not supported yet. | +| `nginx.ingress.kubernetes.io/opentelemetry-trust-incoming-span` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-modsecurity` | Not supported yet. | +| `nginx.ingress.kubernetes.io/enable-owasp-core-rules` | Not supported yet. | +| `nginx.ingress.kubernetes.io/modsecurity-transaction-id` | Not supported yet. | +| `nginx.ingress.kubernetes.io/modsecurity-snippet` | Not supported yet. | +| `nginx.ingress.kubernetes.io/mirror-request-body` | Not supported yet. | +| `nginx.ingress.kubernetes.io/mirror-target` | Not supported yet. | +| `nginx.ingress.kubernetes.io/mirror-host` | Not supported yet. | +| `nginx.ingress.kubernetes.io/x-forwarded-prefix` | Not supported yet. | +| `nginx.ingress.kubernetes.io/upstream-hash-by` | Not supported yet. | +| `nginx.ingress.kubernetes.io/upstream-vhost` | Not supported yet. | +| `nginx.ingress.kubernetes.io/denylist-source-range` | Not supported yet. | +| `nginx.ingress.kubernetes.io/whitelist-source-range` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-buffering` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-buffers-number` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-buffer-size` | Not supported yet. | +| `nginx.ingress.kubernetes.io/proxy-max-temp-file-size` | Not supported yet. | +| `nginx.ingress.kubernetes.io/stream-snippet` | Not supported yet. | From a59bcb29b55c2229f5376bd65fb827168e024301 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 23 Jul 2025 09:56:04 +0200 Subject: [PATCH 12/18] Improve integration tests --- .github/workflows/test-integration.yaml | 33 ++++++++++++++++++++++--- Makefile | 2 +- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 806148268..9eaa2880a 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -34,7 +34,21 @@ jobs: run: touch webui/static/index.html - name: Build binary - run: make binary + run: make binary-linux-amd64 + + - name: Save go cache build + uses: actions/cache/save@v4 + with: + path: | + ~/.cache/go-build + key: ${{ runner.os }}-go-build-cache-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} + + - name: Artifact traefik binary + uses: actions/upload-artifact@v4 + with: + name: traefik + path: ./dist/linux/amd64/traefik + retention-days: 1 test-integration: runs-on: ubuntu-latest @@ -61,8 +75,21 @@ jobs: - name: Avoid generating webui run: touch webui/static/index.html - - name: Build binary - run: make binary + - name: Download traefik binary + uses: actions/download-artifact@v4 + with: + name: traefik + path: ./dist/linux/amd64/ + + - name: Make binary executable + run: chmod +x ./dist/linux/amd64/traefik + + - name: Restore go cache build + uses: actions/cache/restore@v4 + with: + path: | + ~/.cache/go-build + key: ${{ runner.os }}-go-build-cache-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - name: Generate go test Slice id: test_split diff --git a/Makefile b/Makefile index f435fdc2d..b3a7f58b5 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ test-unit: .PHONY: test-integration #? test-integration: Run the integration tests -test-integration: binary +test-integration: GOOS=$(GOOS) GOARCH=$(GOARCH) go test ./integration -test.timeout=20m -failfast -v $(TESTFLAGS) .PHONY: test-ui-unit From c6daab54e33033e34c1d5c6fe236e2fb7939fc46 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 23 Jul 2025 10:34:04 +0200 Subject: [PATCH 13/18] Prepare release v2.11.28 --- CHANGELOG.md | 11 +++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d46ff3bd..c9731bc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [v2.11.28](https://github.com/traefik/traefik/tree/v2.11.28) (2025-07-23) +[All Commits](https://github.com/traefik/traefik/compare/v2.11.27...v2.11.28) + +**Bug fixes:** +- **[logs]** Redact logged install configuration ([#11907](https://github.com/traefik/traefik/pull/11907) by [jspdown](https://github.com/jspdown)) +- **[plugins]** Fix client arbitrary file access during archive extraction zipslip ([#11911](https://github.com/traefik/traefik/pull/11911) by [odaysec](https://github.com/odaysec)) +- **[server]** Disable MPTCP by default ([#11918](https://github.com/traefik/traefik/pull/11918) by [rtribotte](https://github.com/rtribotte)) + +**Documentation:** +- **[k8s/crd,k8s]** Remove all mentions of ordering for TLSOption CurvePreferences field ([#11924](https://github.com/traefik/traefik/pull/11924) by [jnoordsij](https://github.com/jnoordsij)) + ## [v2.11.27](https://github.com/traefik/traefik/tree/v2.11.27) (2025-07-11) [All Commits](https://github.com/traefik/traefik/compare/v2.11.26...v2.11.27) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index 6cb9289b0..6bc515de5 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v2.11.27 +# example new bugfix v2.11.28 CurrentRef = "v2.11" -PreviousRef = "v2.11.26" +PreviousRef = "v2.11.27" BaseBranch = "v2.11" -FutureCurrentRefName = "v2.11.27" +FutureCurrentRefName = "v2.11.28" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 From 9bf14b67644cb1da14923bda0e3144a0355b4543 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 23 Jul 2025 11:16:04 +0200 Subject: [PATCH 14/18] Prepare release v3.4.5 --- CHANGELOG.md | 13 +++++++++++++ script/gcg/traefik-bugfix.toml | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f03e159..bd5768c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [v3.4.5](https://github.com/traefik/traefik/tree/v3.4.5) (2025-07-23) +[All Commits](https://github.com/traefik/traefik/compare/v3.4.4...v3.4.5) + +**Bug fixes:** +- **[http3]** Bump github.com/quic-go/quic-go to v0.54.0 ([#11919](https://github.com/traefik/traefik/pull/11919) by [GreyXor](https://github.com/GreyXor)) + +**Documentation:** +- Fix typo in entrypoints page ([#11914](https://github.com/traefik/traefik/pull/11914) by [adk-swisstopo](https://github.com/adk-swisstopo)) + +**Misc:** +- Merge branch v2.11 into v3.4 ([#11930](https://github.com/traefik/traefik/pull/11930) by [kevinpollet](https://github.com/kevinpollet)) +- Merge branch v2.11 into v3.4 ([#11926](https://github.com/traefik/traefik/pull/11926) by [rtribotte](https://github.com/rtribotte)) + ## [v2.11.28](https://github.com/traefik/traefik/tree/v2.11.28) (2025-07-23) [All Commits](https://github.com/traefik/traefik/compare/v2.11.27...v2.11.28) diff --git a/script/gcg/traefik-bugfix.toml b/script/gcg/traefik-bugfix.toml index a1b6f4ab9..a2bdb8be3 100644 --- a/script/gcg/traefik-bugfix.toml +++ b/script/gcg/traefik-bugfix.toml @@ -4,11 +4,11 @@ RepositoryName = "traefik" OutputType = "file" FileName = "traefik_changelog.md" -# example new bugfix v3.4.4 +# example new bugfix v3.4.5 CurrentRef = "v3.4" -PreviousRef = "v3.4.3" +PreviousRef = "v3.4.4" BaseBranch = "v3.4" -FutureCurrentRefName = "v3.4.4" +FutureCurrentRefName = "v3.4.5" ThresholdPreviousRef = 10 ThresholdCurrentRef = 10 From 2ed2123fc08248adc0c6b59cf915611b387edc38 Mon Sep 17 00:00:00 2001 From: bluepuma77 Date: Wed, 23 Jul 2025 11:40:04 +0200 Subject: [PATCH 15/18] Add constraints key limitations for label providers --- .../reference/install-configuration/providers/docker.md | 4 ++++ .../providers/hashicorp/consul-catalog.md | 4 ++++ .../install-configuration/providers/hashicorp/nomad.md | 4 ++++ .../reference/install-configuration/providers/others/ecs.md | 4 ++++ .../reference/install-configuration/providers/swarm.md | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/docs/content/reference/install-configuration/providers/docker.md b/docs/content/reference/install-configuration/providers/docker.md index 82463e8fc..579223889 100644 --- a/docs/content/reference/install-configuration/providers/docker.md +++ b/docs/content/reference/install-configuration/providers/docker.md @@ -270,6 +270,10 @@ created. If the expression is empty, all detected containers are included. The expression syntax is based on the `Label("key", "value")`, and `LabelRegex("key", "value")` functions, as well as the usual boolean logic, as shown in examples below. +!!! tip "Constraints key limitations" + + Note that `traefik.*` is a reserved label namespace for configuration and can not be used as a key for custom constraints. + ??? example "Constraints Expression Examples" ```toml diff --git a/docs/content/reference/install-configuration/providers/hashicorp/consul-catalog.md b/docs/content/reference/install-configuration/providers/hashicorp/consul-catalog.md index 975c0685d..dce7a3627 100644 --- a/docs/content/reference/install-configuration/providers/hashicorp/consul-catalog.md +++ b/docs/content/reference/install-configuration/providers/hashicorp/consul-catalog.md @@ -112,6 +112,10 @@ created. If the expression is empty, all detected services are included. The expression syntax is based on the ```Tag(`tag`)```, and ```TagRegex(`tag`)``` functions, as well as the usual boolean logic, as shown in examples below. +!!! tip "Constraints key limitations" + + Note that `traefik.*` is a reserved label namespace for configuration and can not be used as a key for custom constraints. + ??? example "Constraints Expression Examples" ```toml diff --git a/docs/content/reference/install-configuration/providers/hashicorp/nomad.md b/docs/content/reference/install-configuration/providers/hashicorp/nomad.md index 19608cd94..bb2ac3a39 100644 --- a/docs/content/reference/install-configuration/providers/hashicorp/nomad.md +++ b/docs/content/reference/install-configuration/providers/hashicorp/nomad.md @@ -191,6 +191,10 @@ created. If the expression is empty, all detected services are included. The expression syntax is based on the ```Tag(`tag`)```, and ```TagRegex(`tag`)``` functions, as well as the usual boolean logic, as shown in examples below. +!!! tip "Constraints key limitations" + + Note that `traefik.*` is a reserved label namespace for configuration and can not be used as a key for custom constraints. + ??? example "Constraints Expression Examples" ```toml diff --git a/docs/content/reference/install-configuration/providers/others/ecs.md b/docs/content/reference/install-configuration/providers/others/ecs.md index f6aeb2731..7f50032ef 100644 --- a/docs/content/reference/install-configuration/providers/others/ecs.md +++ b/docs/content/reference/install-configuration/providers/others/ecs.md @@ -49,6 +49,10 @@ If the expression is empty, all detected containers are included. The expression syntax is based on the `Label("key", "value")`, and `LabelRegex("key", "value")` functions, as well as the usual boolean logic, as shown in examples below. +!!! tip "Constraints key limitations" + + Note that `traefik.*` is a reserved label namespace for configuration and can not be used as a key for custom constraints. + ??? example "Constraints Expression Examples" ```toml diff --git a/docs/content/reference/install-configuration/providers/swarm.md b/docs/content/reference/install-configuration/providers/swarm.md index 64d058a87..96ec463c5 100644 --- a/docs/content/reference/install-configuration/providers/swarm.md +++ b/docs/content/reference/install-configuration/providers/swarm.md @@ -276,6 +276,10 @@ created. If the expression is empty, all detected containers are included. The expression syntax is based on the `Label("key", "value")`, and `LabelRegex("key", "value")` functions, as well as the usual boolean logic, as shown in examples below. +!!! tip "Constraints key limitations" + + Note that `traefik.*` is a reserved label namespace for configuration and can not be used as a key for custom constraints. + ??? example "Constraints Expression Examples" ```toml From 43162507e33e573f4cd4acd6d20a1297f3623e76 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 23 Jul 2025 12:04:04 +0200 Subject: [PATCH 16/18] Add a note for the removal of default MPTCP enablement in the migration guide --- docs/content/migration/v3.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/content/migration/v3.md b/docs/content/migration/v3.md index 6581b5f39..dc29ae058 100644 --- a/docs/content/migration/v3.md +++ b/docs/content/migration/v3.md @@ -319,3 +319,14 @@ 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.4.5 + +### MultiPath TCP + +Since `v3.4.5`, the MultiPath TCP support introduced with `v3.4.2` has been removed. +It appears that enabling MPTCP on some platforms can cause Traefik to stop with the following error logs message: + +- `set tcp X.X.X.X:X->X.X.X.X:X: setsockopt: operation not supported` + +However, it can be re-enabled by setting the `multipathtcp` variable in the GODEBUG environment variable, see the related [go documentation](https://go.dev/doc/godebug#go-124). From 4576155005968ddf5143f29d8130b086394ae709 Mon Sep 17 00:00:00 2001 From: Kevin Pollet Date: Wed, 23 Jul 2025 15:04:08 +0200 Subject: [PATCH 17/18] Remove dead link to Peka blog --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 652b4e5b7..a0e0785fe 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ We use [Semantic Versioning](https://semver.org/). ## Credits -Kudos to [Peka](http://peka.byethost11.com/photoblog/) for his awesome work on the gopher's logo!. +Kudos to Peka for his awesome work on the gopher's logo!. The gopher's logo of Traefik is licensed under the Creative Commons 3.0 Attributions license. From 2cbd96e64c929bea8f933efa75080e9656314f34 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 23 Jul 2025 15:46:04 +0200 Subject: [PATCH 18/18] Prepare release v3.5.0 --- .github/PULL_REQUEST_TEMPLATE.md | 15 ++++--- CHANGELOG.md | 44 +++++++++++++++++++++ script/gcg/traefik-final-release-part1.toml | 10 ++--- script/gcg/traefik-final-release-part2.toml | 8 ++-- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2594684f6..b4717dc18 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,17 +1,16 @@