1
0
mirror of https://github.com/containous/traefik.git synced 2024-12-22 13:34:03 +03:00

Manage observability at entrypoint and router level

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Romain 2024-12-12 09:52:07 +01:00 committed by GitHub
parent 9588e51146
commit b1934231ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1216 additions and 303 deletions

View File

@ -68,6 +68,7 @@ metrics:
```bash tab="CLI"
--metrics.datadog.addEntryPointsLabels=true
```
#### `addRoutersLabels`
_Optional, Default=false_

View File

@ -5,16 +5,80 @@ description: "Traefik provides Logs, Access Logs, Metrics and Tracing. Read the
# Overview
Traefik's Observability system
{: .subtitle }
Traefiks observability features include logs, access logs, metrics, and tracing. You can configure these options globally or at more specific levels, such as per router or per entry point.
## Logs
## Configuration Example
Enable access logs, metrics, and tracing globally
```yaml tab="File (YAML)"
accessLog: {}
metrics:
otlp: {}
tracing: {}
```
```yaml tab="File (TOML)"
[accessLog]
[metrics]
[metrics.otlp]
[tracing]
```
```bash tab="CLI"
--accesslog=true
--metrics.otlp=true
--tracing=true
```
You can disable access logs, metrics, and tracing for a specific entrypoint attached to a router:
```yaml tab="File (YAML)"
# Static Configuration
entryPoints:
EntryPoint0:
address: ':8000/udp'
observability:
accessLogs: false
tracing: false
metrics: false
```
```toml tab="File (TOML)"
# Static Configuration
[entryPoints.EntryPoint0]
address = ":8000/udp"
[entryPoints.EntryPoint0.observability]
accessLogs = false
tracing = false
metrics = false
```
```bash tab="CLI"
# Static Configuration
--entryPoints.EntryPoint0.address=:8000/udp
--entryPoints.EntryPoint0.observability.accessLogs=false
--entryPoints.EntryPoint0.observability.metrics=false
--entryPoints.EntryPoint0.observability.tracing=false
```
!!!note "Default Behavior"
A router with its own observability configuration will override the global default.
## Configuration Options
### Logs
Traefik logs informs about everything that happens within Traefik (startup, configuration, events, shutdown, and so on).
Read the [Logs documentation](./logs.md) to learn how to configure it.
## Access Logs
### Access Logs
Access logs are a key part of observability in Traefik.
@ -24,7 +88,7 @@ including the source IP address, requested URL, response status code, and more.
Read the [Access Logs documentation](./access-logs.md) to learn how to configure it.
## Metrics
### Metrics
Traefik offers a metrics feature that provides valuable insights about the performance and usage.
These metrics include the number of requests received, the requests duration, and more.
@ -33,7 +97,7 @@ On top of supporting metrics in the OpenTelemetry format, Traefik supports the f
Read the [Metrics documentation](./metrics/overview.md) to learn how to configure it.
## Tracing
### Tracing
The Traefik tracing system allows developers to gain deep visibility into the flow of requests through their infrastructure.

View File

@ -147,6 +147,9 @@
- "traefik.http.middlewares.middleware25.stripprefixregex.regex=foobar, foobar"
- "traefik.http.routers.router0.entrypoints=foobar, foobar"
- "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.tracing=true"
- "traefik.http.routers.router0.priority=42"
- "traefik.http.routers.router0.rule=foobar"
- "traefik.http.routers.router0.rulesyntax=foobar"
@ -160,6 +163,9 @@
- "traefik.http.routers.router0.tls.options=foobar"
- "traefik.http.routers.router1.entrypoints=foobar, foobar"
- "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.tracing=true"
- "traefik.http.routers.router1.priority=42"
- "traefik.http.routers.router1.rule=foobar"
- "traefik.http.routers.router1.rulesyntax=foobar"

View File

@ -20,6 +20,10 @@
[[http.routers.Router0.tls.domains]]
main = "foobar"
sans = ["foobar", "foobar"]
[http.routers.Router0.observability]
accessLogs = true
tracing = true
metrics = true
[http.routers.Router1]
entryPoints = ["foobar", "foobar"]
middlewares = ["foobar", "foobar"]
@ -38,6 +42,10 @@
[[http.routers.Router1.tls.domains]]
main = "foobar"
sans = ["foobar", "foobar"]
[http.routers.Router1.observability]
accessLogs = true
tracing = true
metrics = true
[http.services]
[http.services.Service01]
[http.services.Service01.failover]

View File

@ -25,6 +25,10 @@ http:
sans:
- foobar
- foobar
observability:
accessLogs: true
tracing: true
metrics: true
Router1:
entryPoints:
- foobar
@ -48,6 +52,10 @@ http:
sans:
- foobar
- foobar
observability:
accessLogs: true
tracing: true
metrics: true
services:
Service01:
failover:

View File

@ -86,6 +86,18 @@ spec:
- name
type: object
type: array
observability:
description: |-
Observability defines the observability configuration for a router.
More info: https://doc.traefik.io/traefik/v3.2/routing/routers/#observability
properties:
accessLogs:
type: boolean
metrics:
type: boolean
tracing:
type: boolean
type: object
priority:
description: |-
Priority defines the router's priority.

View File

@ -173,6 +173,9 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/routers/Router0/entryPoints/1` | `foobar` |
| `traefik/http/routers/Router0/middlewares/0` | `foobar` |
| `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/tracing` | `true` |
| `traefik/http/routers/Router0/priority` | `42` |
| `traefik/http/routers/Router0/rule` | `foobar` |
| `traefik/http/routers/Router0/ruleSyntax` | `foobar` |
@ -189,6 +192,9 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/routers/Router1/entryPoints/1` | `foobar` |
| `traefik/http/routers/Router1/middlewares/0` | `foobar` |
| `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/tracing` | `true` |
| `traefik/http/routers/Router1/priority` | `42` |
| `traefik/http/routers/Router1/rule` | `foobar` |
| `traefik/http/routers/Router1/ruleSyntax` | `foobar` |

View File

@ -86,6 +86,18 @@ spec:
- name
type: object
type: array
observability:
description: |-
Observability defines the observability configuration for a router.
More info: https://doc.traefik.io/traefik/v3.2/routing/routers/#observability
properties:
accessLogs:
type: boolean
metrics:
type: boolean
tracing:
type: boolean
type: object
priority:
description: |-
Priority defines the router's priority.

View File

@ -264,6 +264,15 @@ HTTP/3 configuration. (Default: ```false```)
`--entrypoints.<name>.http3.advertisedport`:
UDP port to advertise, on which HTTP/3 is available. (Default: ```0```)
`--entrypoints.<name>.observability.accesslogs`:
(Default: ```true```)
`--entrypoints.<name>.observability.metrics`:
(Default: ```true```)
`--entrypoints.<name>.observability.tracing`:
(Default: ```true```)
`--entrypoints.<name>.proxyprotocol`:
Proxy-Protocol configuration. (Default: ```false```)

View File

@ -264,6 +264,15 @@ Subject alternative names.
`TRAEFIK_ENTRYPOINTS_<NAME>_HTTP_TLS_OPTIONS`:
Default TLS options for the routers linked to the entry point.
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_ACCESSLOGS`:
(Default: ```true```)
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_METRICS`:
(Default: ```true```)
`TRAEFIK_ENTRYPOINTS_<NAME>_OBSERVABILITY_TRACING`:
(Default: ```true```)
`TRAEFIK_ENTRYPOINTS_<NAME>_PROXYPROTOCOL`:
Proxy-Protocol configuration. (Default: ```false```)

View File

@ -77,6 +77,10 @@
advertisedPort = 42
[entryPoints.EntryPoint0.udp]
timeout = "42s"
[entryPoints.EntryPoint0.observability]
accessLogs = true
tracing = true
metrics = true
[providers]
providersThrottleDuration = "42s"

View File

@ -91,6 +91,10 @@ entryPoints:
advertisedPort: 42
udp:
timeout: 42s
observability:
accessLogs: true
tracing: true
metrics: true
providers:
providersThrottleDuration: 42s
docker:

View File

@ -1237,8 +1237,6 @@ entryPoints:
--entryPoints.foo.udp.timeout=10s
```
{!traefik-for-business-applications.md!}
## Systemd Socket Activation
Traefik supports [systemd socket activation](https://www.freedesktop.org/software/systemd/man/latest/systemd-socket-activate.html).
@ -1260,3 +1258,105 @@ systemd-socket-activate -l 80 -l 443 --fdname web:websecure ./traefik --entrypo
!!! warning "Docker Support"
Socket activation is not supported by Docker but works with Podman containers.
## Observability Options
This section is dedicated to options to control observability for an EntryPoint.
!!! info "Note that you must first enable access-logs, tracing, and/or metrics."
!!! warning "AddInternals option"
By default, and for any type of signals (access-logs, metrics and tracing),
Traefik disables observability for internal resources.
The observability options described below cannot interfere with the `AddInternals` ones,
and will be ignored.
For instance, if a router exposes the `api@internal` service and `metrics.AddInternals` is false,
it will never produces metrics, even if the EntryPoint observability configuration enables metrics.
### AccessLogs
_Optional, Default=true_
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.
```yaml tab="File (YAML)"
entryPoints:
foo:
address: ':8000/udp'
observability:
accessLogs: false
```
```toml tab="File (TOML)"
[entryPoints.foo]
address = ":8000/udp"
[entryPoints.foo.observability]
accessLogs = false
```
```bash tab="CLI"
--entryPoints.foo.address=:8000/udp
--entryPoints.foo.observability.accessLogs=false
```
### Metrics
_Optional, Default=true_
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.
```yaml tab="File (YAML)"
entryPoints:
foo:
address: ':8000/udp'
observability:
metrics: false
```
```toml tab="File (TOML)"
[entryPoints.foo]
address = ":8000/udp"
[entryPoints.foo.observability]
metrics = false
```
```bash tab="CLI"
--entryPoints.foo.address=:8000/udp
--entryPoints.foo.observability.metrics=false
```
### Tracing
_Optional, Default=true_
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.
```yaml tab="File (YAML)"
entryPoints:
foo:
address: ':8000/udp'
observability:
tracing: false
```
```toml tab="File (TOML)"
[entryPoints.foo]
address = ":8000/udp"
[entryPoints.foo.observability]
tracing = false
```
```bash tab="CLI"
--entryPoints.foo.address=:8000/udp
--entryPoints.foo.observability.tracing=false
```
{!traefik-for-business-applications.md!}

View File

@ -111,6 +111,30 @@ For example, to change the rule, you could add the tag ```traefik.http.routers.m
traefik.http.routers.myrouter.tls.options=foobar
```
??? info "`traefik.http.routers.<router_name>.observability.accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
```yaml
traefik.http.routers.myrouter.observability.accesslogs=true
```
??? info "`traefik.http.routers.<router_name>.observability.metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
```yaml
traefik.http.routers.myrouter.observability.metrics=true
```
??? info "`traefik.http.routers.<router_name>.observability.tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
```yaml
traefik.http.routers.myrouter.observability.tracing=true
```
??? info "`traefik.http.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority) for more information.

View File

@ -224,6 +224,30 @@ For example, to change the rule, you could add the label ```traefik.http.routers
- "traefik.http.routers.myrouter.tls.options=foobar"
```
??? info "`traefik.http.routers.<router_name>.observability.accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
```yaml
- "traefik.http.routers.myrouter.observability.accesslogs=true"
```
??? info "`traefik.http.routers.<router_name>.observability.metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
```yaml
- "traefik.http.routers.myrouter.observability.metrics=true"
```
??? info "`traefik.http.routers.<router_name>.observability.tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
```yaml
- "traefik.http.routers.myrouter.observability.tracing=true"
```
??? info "`traefik.http.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority) for more information.

View File

@ -111,6 +111,30 @@ For example, to change the rule, you could add the label ```traefik.http.routers
traefik.http.routers.myrouter.tls.options=foobar
```
??? info "`traefik.http.routers.<router_name>.observability.accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
```yaml
traefik.http.routers.myrouter.observability.accesslogs=true
```
??? info "`traefik.http.routers.<router_name>.observability.metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
```yaml
traefik.http.routers.myrouter.observability.metrics=true
```
??? info "`traefik.http.routers.<router_name>.observability.tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
```yaml
traefik.http.routers.myrouter.observability.tracing=true
```
??? info "`traefik.http.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority) for more information.

View File

@ -332,17 +332,21 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne
middlewares: # [5]
- name: middleware1 # [6]
namespace: default # [7]
services: # [8]
observability: # [8]
accesslogs: true # [9]
metrics: true # [10]
tracing: true # [11]
services: # [12]
- kind: Service
name: foo
namespace: default
passHostHeader: true
port: 80 # [9]
port: 80 # [13]
responseForwarding:
flushInterval: 1ms
scheme: https
serversTransport: transport # [10]
healthCheck: # [11]
serversTransport: transport # [14]
healthCheck: # [15]
path: /health
interval: 15s
sticky:
@ -355,17 +359,17 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne
path: /foo
strategy: RoundRobin
weight: 10
nativeLB: true # [12]
nodePortLB: true # [13]
tls: # [14]
secretName: supersecret # [15]
options: # [16]
name: opt # [17]
namespace: default # [18]
certResolver: foo # [19]
domains: # [20]
- main: example.net # [21]
sans: # [22]
nativeLB: true # [16]
nodePortLB: true # [17]
tls: # [18]
secretName: supersecret # [19]
options: # [20]
name: opt # [21]
namespace: default # [22]
certResolver: foo # [23]
domains: # [24]
- main: example.net # [25]
sans: # [26]
- a.example.net
- b.example.net
```
@ -379,21 +383,25 @@ Register the `IngressRoute` [kind](../../reference/dynamic-configuration/kuberne
| [5] | `routes[n].middlewares` | List of reference to [Middleware](#kind-middleware) |
| [6] | `middlewares[n].name` | Defines the [Middleware](#kind-middleware) name |
| [7] | `middlewares[n].namespace` | Defines the [Middleware](#kind-middleware) namespace. It can be omitted when the Middleware is in the IngressRoute namespace. |
| [8] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) |
| [9] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. |
| [10] | `services[n].serversTransport` | Defines the reference to a [ServersTransport](#kind-serverstransport). The ServersTransport namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace (see [ServersTransport reference](#serverstransport-reference)). |
| [11] | `services[n].healthCheck` | Defines the HealthCheck when service references a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName. |
| [12] | `services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. |
| [13] | `services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. |
| [14] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration |
| [15] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [16] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [17] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [18] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [19] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) |
| [20] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [21] | `domains[n].main` | Defines the main domain name |
| [22] | `domains[n].sans` | List of SANs (alternative domains) |
| [8] | `routes[n].observability` | Defines the route observability configuration. |
| [9] | `observability.accesslogs` | Defines whether the route will produce [access-logs](../routers/index.md#accesslogs). |
| [10] | `observability.metrics` | Defines whether the route will produce [metrics](../routers/index.md#metrics). |
| [11] | `observability.tracing` | Defines whether the route will produce [traces](../routers/index.md#tracing). |
| [12] | `routes[n].services` | List of any combination of [TraefikService](#kind-traefikservice) and reference to a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) (See below for `ExternalName Service` setup) |
| [13] | `services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. |
| [14] | `services[n].serversTransport` | Defines the reference to a [ServersTransport](#kind-serverstransport). The ServersTransport namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace (see [ServersTransport reference](#serverstransport-reference)). |
| [15] | `services[n].healthCheck` | Defines the HealthCheck when service references a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName. |
| [16] | `services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. |
| [17] | `services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. |
| [18] | `tls` | Defines [TLS](../routers/index.md#tls) certificate configuration |
| [19] | `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace) |
| [20] | `tls.options` | Defines the reference to a [TLSOption](#kind-tlsoption) |
| [21] | `options.name` | Defines the [TLSOption](#kind-tlsoption) name |
| [22] | `options.namespace` | Defines the [TLSOption](#kind-tlsoption) namespace |
| [23] | `tls.certResolver` | Defines the reference to a [CertResolver](../routers/index.md#certresolver) |
| [24] | `tls.domains` | List of [domains](../routers/index.md#domains) |
| [25] | `domains[n].main` | Defines the main domain name |
| [26] | `domains[n].sans` | List of SANs (alternative domains) |
??? example "Declaring an IngressRoute"

View File

@ -288,6 +288,30 @@ which in turn will create the resulting routers, services, handlers, etc.
traefik.ingress.kubernetes.io/router.tls.options: foobar@file
```
??? info "`traefik.ingress.kubernetes.io/router.observability.accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
```yaml
traefik.ingress.kubernetes.io/router.observability.accesslogs: true
```
??? info "`traefik.ingress.kubernetes.io/router.observability.metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
```yaml
traefik.ingress.kubernetes.io/router.observability.metrics: true
```
??? info "`traefik.ingress.kubernetes.io/router.observability.tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
```yaml
traefik.ingress.kubernetes.io/router.observability.tracing: true
```
#### On Service
??? info "`traefik.ingress.kubernetes.io/service.nativelb`"

View File

@ -95,6 +95,30 @@ A Story of key & values
|---------------------------------------------|----------|
| `traefik/http/routers/myrouter/tls/options` | `foobar` |
??? info "`traefik/http/routers/<router_name>/observability/accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
| Key (Path) | Value |
|----------------------------------------------------------|--------|
| `traefik/http/routers/myrouter/observability/accesslogs` | `true` |
??? info "`traefik/http/routers/<router_name>/observability/metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
| Key (Path) | Value |
|-------------------------------------------------------|--------|
| `traefik/http/routers/myrouter/observability/metrics` | `true` |
??? info "`traefik/http/routers/<router_name>/observability/tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
| Key (Path) | Value |
|-------------------------------------------------------|--------|
| `traefik/http/routers/myrouter/observability/tracing` | `true` |
??? info "`traefik/http/routers/<router_name>/priority`"
See [priority](../routers/index.md#priority) for more information.

View File

@ -111,6 +111,30 @@ For example, to change the rule, you could add the tag ```traefik.http.routers.m
traefik.http.routers.myrouter.tls.options=foobar
```
??? info "`traefik.http.routers.<router_name>.observability.accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
```yaml
traefik.http.routers.myrouter.observability.accesslogs=true
```
??? info "`traefik.http.routers.<router_name>.observability.metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
```yaml
traefik.http.routers.myrouter.observability.metrics=true
```
??? info "`traefik.http.routers.<router_name>.observability.tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
```yaml
traefik.http.routers.myrouter.observability.tracing=true
```
??? info "`traefik.http.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority) for more information.

View File

@ -7,7 +7,8 @@ There are, however, exceptions when using label-based configurations:
and a label defines a service (e.g. implicitly through a loadbalancer server port value),
but the router does not specify any service,
then that service is automatically assigned to the router.
1. If a label defines a router (e.g. through a router Rule) but no service is defined,
2. If a label defines a router (e.g. through a router Rule) but no service is defined,
then a service is automatically created and assigned to the router.
!!! info ""

View File

@ -235,6 +235,30 @@ For example, to change the rule, you could add the label ```traefik.http.routers
- "traefik.http.routers.myrouter.tls.options=foobar"
```
??? info "`traefik.http.routers.<router_name>.observability.accesslogs`"
See accesslogs [option](../routers/index.md#accesslogs) for more information.
```yaml
- "traefik.http.routers.myrouter.observability.accesslogs=true"
```
??? info "`traefik.http.routers.<router_name>.observability.metrics`"
See metrics [option](../routers/index.md#metrics) for more information.
```yaml
- "traefik.http.routers.myrouter.observability.metrics=true"
```
??? info "`traefik.http.routers.<router_name>.observability.tracing`"
See tracing [option](../routers/index.md#tracing) for more information.
```yaml
- "traefik.http.routers.myrouter.observability.tracing=true"
```
??? info "`traefik.http.routers.<router_name>.priority`"
See [priority](../routers/index.md#priority) for more information.

View File

@ -877,6 +877,117 @@ The [supported `provider` table](../../https/acme.md#providers) indicates if the
!!! warning "Double Wildcard Certificates"
It is not possible to request a double wildcard certificate for a domain (for example `*.*.local.com`).
### Observability
The Observability section defines a per router behavior regarding access-logs, metrics or tracing.
The default router observability configuration is inherited from the attached EntryPoints and can be configured with the observability [options](../../routing/entrypoints.md#observability-options).
However, a router defining its own observability configuration will opt-out from these defaults.
!!! info "Note that to enable router-level observability, you must first enable access-logs, tracing, and/or metrics."
!!! warning "AddInternals option"
By default, and for any type of signals (access-logs, metrics and tracing),
Traefik disables observability for internal resources.
The observability options described below cannot interfere with the `AddInternals` ones,
and will be ignored.
For instance, if a router exposes the `api@internal` service and `metrics.AddInternals` is false,
it will never produces metrics, even if the router observability configuration enables metrics.
#### `accessLogs`
_Optional_
The `accessLogs` option controls whether the router will produce access-logs.
??? example "Disable access-logs for a router using the [File Provider](../../providers/file.md)"
```yaml tab="YAML"
## Dynamic configuration
http:
routers:
my-router:
rule: "Path(`/foo`)"
service: service-foo
observability:
accessLogs: false
```
```toml tab="TOML"
## Dynamic configuration
[http.routers]
[http.routers.my-router]
rule = "Path(`/foo`)"
service = "service-foo"
[http.routers.my-router.observability]
accessLogs = false
```
#### `metrics`
_Optional_
The `metrics` option controls whether the router will produce metrics.
!!! warning "Metrics layers"
When metrics layers are not enabled with the `addEntryPointsLabels`, `addRoutersLabels` and/or `addServicesLabels` options,
enabling metrics for a router will not enable them.
??? example "Disable metrics for a router using the [File Provider](../../providers/file.md)"
```yaml tab="YAML"
## Dynamic configuration
http:
routers:
my-router:
rule: "Path(`/foo`)"
service: service-foo
observability:
metrics: false
```
```toml tab="TOML"
## Dynamic configuration
[http.routers]
[http.routers.my-router]
rule = "Path(`/foo`)"
service = "service-foo"
[http.routers.my-router.observability]
metrics = false
```
#### `tracing`
_Optional_
The `tracing` option controls whether the router will produce traces.
??? example "Disable tracing for a router using the [File Provider](../../providers/file.md)"
```yaml tab="YAML"
## Dynamic configuration
http:
routers:
my-router:
rule: "Path(`/foo`)"
service: service-foo
observability:
tracing: false
```
```toml tab="TOML"
## Dynamic configuration
[http.routers]
[http.routers.my-router]
rule = "Path(`/foo`)"
service = "service-foo"
[http.routers.my-router.observability]
tracing = false
```
## Configuring TCP Routers
!!! warning "The character `@` is not authorized in the router name"

View File

@ -86,6 +86,18 @@ spec:
- name
type: object
type: array
observability:
description: |-
Observability defines the observability configuration for a router.
More info: https://doc.traefik.io/traefik/v3.2/routing/routers/#observability
properties:
accessLogs:
type: boolean
metrics:
type: boolean
tracing:
type: boolean
type: object
priority:
description: |-
Priority defines the router's priority.

View File

@ -36,11 +36,12 @@ type HTTPConfiguration struct {
// +k8s:deepcopy-gen=true
// Model is a set of default router's values.
// Model holds model configuration.
type Model struct {
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
Observability RouterObservabilityConfig `json:"observability,omitempty" toml:"observability,omitempty" yaml:"observability,omitempty" export:"true"`
DefaultRuleSyntax string `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
@ -57,14 +58,15 @@ type Service struct {
// Router holds the router configuration.
type Router struct {
EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"`
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
RuleSyntax string `json:"ruleSyntax,omitempty" toml:"ruleSyntax,omitempty" yaml:"ruleSyntax,omitempty" export:"true"`
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
EntryPoints []string `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"`
Middlewares []string `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty" export:"true"`
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`
Rule string `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
RuleSyntax string `json:"ruleSyntax,omitempty" toml:"ruleSyntax,omitempty" yaml:"ruleSyntax,omitempty" export:"true"`
Priority int `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty" export:"true"`
TLS *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"`
Observability *RouterObservabilityConfig `json:"observability,omitempty" toml:"observability,omitempty" yaml:"observability,omitempty" export:"true"`
DefaultRule bool `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
}
// +k8s:deepcopy-gen=true
@ -78,6 +80,15 @@ type RouterTLSConfig struct {
// +k8s:deepcopy-gen=true
// RouterObservabilityConfig holds the observability configuration for a router.
type RouterObservabilityConfig 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"`
}
// +k8s:deepcopy-gen=true
// Mirroring holds the Mirroring configuration.
type Mirroring struct {
Service string `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty" export:"true"`

View File

@ -1023,6 +1023,7 @@ func (in *Model) DeepCopyInto(out *Model) {
*out = new(RouterTLSConfig)
(*in).DeepCopyInto(*out)
}
in.Observability.DeepCopyInto(&out.Observability)
return
}
@ -1249,6 +1250,11 @@ func (in *Router) DeepCopyInto(out *Router) {
*out = new(RouterTLSConfig)
(*in).DeepCopyInto(*out)
}
if in.Observability != nil {
in, out := &in.Observability, &out.Observability
*out = new(RouterObservabilityConfig)
(*in).DeepCopyInto(*out)
}
return
}
@ -1262,6 +1268,37 @@ func (in *Router) DeepCopy() *Router {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterObservabilityConfig) DeepCopyInto(out *RouterObservabilityConfig) {
*out = *in
if in.AccessLogs != nil {
in, out := &in.AccessLogs, &out.AccessLogs
*out = new(bool)
**out = **in
}
if in.Tracing != nil {
in, out := &in.Tracing, &out.Tracing
*out = new(bool)
**out = **in
}
if in.Metrics != nil {
in, out := &in.Metrics, &out.Metrics
*out = new(bool)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouterObservabilityConfig.
func (in *RouterObservabilityConfig) DeepCopy() *RouterObservabilityConfig {
if in == nil {
return nil
}
out := new(RouterObservabilityConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RouterTCPTLSConfig) DeepCopyInto(out *RouterTCPTLSConfig) {
*out = *in

View File

@ -880,6 +880,11 @@ func TestEncodeConfiguration(t *testing.T) {
Rule: "foobar",
Priority: 42,
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
"Router1": {
EntryPoints: []string{
@ -893,6 +898,11 @@ func TestEncodeConfiguration(t *testing.T) {
Service: "foobar",
Rule: "foobar",
Priority: 42,
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
Middlewares: map[string]*dynamic.Middleware{
@ -1405,17 +1415,23 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.aaa": "foo1",
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.bbb": "foo2",
"traefik.HTTP.Routers.Router0.EntryPoints": "foobar, fiibar",
"traefik.HTTP.Routers.Router0.Middlewares": "foobar, fiibar",
"traefik.HTTP.Routers.Router0.Priority": "42",
"traefik.HTTP.Routers.Router0.Rule": "foobar",
"traefik.HTTP.Routers.Router0.Service": "foobar",
"traefik.HTTP.Routers.Router0.TLS": "true",
"traefik.HTTP.Routers.Router1.EntryPoints": "foobar, fiibar",
"traefik.HTTP.Routers.Router1.Middlewares": "foobar, fiibar",
"traefik.HTTP.Routers.Router1.Priority": "42",
"traefik.HTTP.Routers.Router1.Rule": "foobar",
"traefik.HTTP.Routers.Router1.Service": "foobar",
"traefik.HTTP.Routers.Router0.EntryPoints": "foobar, fiibar",
"traefik.HTTP.Routers.Router0.Middlewares": "foobar, fiibar",
"traefik.HTTP.Routers.Router0.Priority": "42",
"traefik.HTTP.Routers.Router0.Rule": "foobar",
"traefik.HTTP.Routers.Router0.Service": "foobar",
"traefik.HTTP.Routers.Router0.TLS": "true",
"traefik.HTTP.Routers.Router0.Observability.AccessLogs": "true",
"traefik.HTTP.Routers.Router0.Observability.Tracing": "true",
"traefik.HTTP.Routers.Router0.Observability.Metrics": "true",
"traefik.HTTP.Routers.Router1.EntryPoints": "foobar, fiibar",
"traefik.HTTP.Routers.Router1.Middlewares": "foobar, fiibar",
"traefik.HTTP.Routers.Router1.Priority": "42",
"traefik.HTTP.Routers.Router1.Rule": "foobar",
"traefik.HTTP.Routers.Router1.Service": "foobar",
"traefik.HTTP.Routers.Router1.Observability.AccessLogs": "true",
"traefik.HTTP.Routers.Router1.Observability.Tracing": "true",
"traefik.HTTP.Routers.Router1.Observability.Metrics": "true",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers.name0": "foobar",
"traefik.HTTP.Services.Service0.LoadBalancer.HealthCheck.Headers.name1": "foobar",

View File

@ -23,6 +23,7 @@ type EntryPoint struct {
HTTP2 *HTTP2Config `description:"HTTP/2 configuration." json:"http2,omitempty" toml:"http2,omitempty" yaml:"http2,omitempty" export:"true"`
HTTP3 *HTTP3Config `description:"HTTP/3 configuration." json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
UDP *UDPConfig `description:"UDP configuration." json:"udp,omitempty" toml:"udp,omitempty" yaml:"udp,omitempty"`
Observability *ObservabilityConfig `description:"Observability configuration." json:"observability,omitempty" toml:"observability,omitempty" yaml:"observability,omitempty" export:"true"`
}
// GetAddress strips any potential protocol part of the address field of the
@ -59,6 +60,8 @@ func (ep *EntryPoint) SetDefaults() {
ep.HTTP.SetDefaults()
ep.HTTP2 = &HTTP2Config{}
ep.HTTP2.SetDefaults()
ep.Observability = &ObservabilityConfig{}
ep.Observability.SetDefaults()
}
// HTTPConfig is the HTTP configuration of an entry point.
@ -158,3 +161,17 @@ type UDPConfig struct {
func (u *UDPConfig) SetDefaults() {
u.Timeout = ptypes.Duration(DefaultUDPTimeout)
}
// 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"`
}
// SetDefaults sets the default values.
func (o *ObservabilityConfig) SetDefaults() {
o.AccessLogs = true
o.Tracing = true
o.Metrics = true
}

View File

@ -77,6 +77,11 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
},
@ -122,6 +127,11 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
CertificatesResolvers: map[string]CertificateResolver{
@ -178,6 +188,11 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
CertificatesResolvers: map[string]CertificateResolver{
@ -238,6 +253,11 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
UDP: &UDPConfig{
Timeout: 3000000000,
},
Observability: &ObservabilityConfig{
AccessLogs: true,
Tracing: true,
Metrics: true,
},
}},
Providers: &Providers{},
CertificatesResolvers: map[string]CertificateResolver{

View File

@ -119,6 +119,11 @@ func (m *metricsMiddleware) GetTracingInformation() (string, string, trace.SpanK
}
func (m *metricsMiddleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if val := req.Context().Value(observability.DisableMetricsKey); val != nil {
m.next.ServeHTTP(rw, req)
return
}
proto := getRequestProtocol(req)
var labels []string

View File

@ -2,21 +2,15 @@ package observability
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/accesslog"
"github.com/traefik/traefik/v3/pkg/tracing"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
)
@ -28,24 +22,19 @@ const (
type entryPointTracing struct {
tracer *tracing.Tracer
entryPoint string
next http.Handler
semConvMetricRegistry *metrics.SemConvMetricsRegistry
entryPoint string
next http.Handler
}
// WrapEntryPointHandler Wraps tracing to alice.Constructor.
func WrapEntryPointHandler(ctx context.Context, tracer *tracing.Tracer, semConvMetricRegistry *metrics.SemConvMetricsRegistry, entryPointName string) alice.Constructor {
// EntryPointHandler Wraps tracing to alice.Constructor.
func EntryPointHandler(ctx context.Context, tracer *tracing.Tracer, entryPointName string) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
if tracer == nil {
tracer = tracing.NewTracer(noop.Tracer{}, nil, nil, nil)
}
return newEntryPoint(ctx, tracer, semConvMetricRegistry, entryPointName, next), nil
return newEntryPoint(ctx, tracer, entryPointName, next), nil
}
}
// newEntryPoint creates a new tracing middleware for incoming requests.
func newEntryPoint(ctx context.Context, tracer *tracing.Tracer, semConvMetricRegistry *metrics.SemConvMetricsRegistry, entryPointName string, next http.Handler) http.Handler {
func newEntryPoint(ctx context.Context, tracer *tracing.Tracer, entryPointName string, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", entryPointTypeName).Debug().Msg("Creating middleware")
if tracer == nil {
@ -53,10 +42,9 @@ func newEntryPoint(ctx context.Context, tracer *tracing.Tracer, semConvMetricReg
}
return &entryPointTracing{
entryPoint: entryPointName,
tracer: tracer,
semConvMetricRegistry: semConvMetricRegistry,
next: next,
entryPoint: entryPointName,
tracer: tracer,
next: next,
}
}
@ -88,23 +76,4 @@ func (e *entryPointTracing) ServeHTTP(rw http.ResponseWriter, req *http.Request)
end := time.Now()
span.End(trace.WithTimestamp(end))
if e.semConvMetricRegistry != nil && e.semConvMetricRegistry.HTTPServerRequestDuration() != nil {
var attrs []attribute.KeyValue
if recorder.Status() < 100 || recorder.Status() >= 600 {
attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code ; %d", recorder.Status())))
} else if recorder.Status() >= 400 {
attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(recorder.Status())))
}
attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
attrs = append(attrs, semconv.HTTPResponseStatusCode(recorder.Status()))
attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
attrs = append(attrs, semconv.NetworkProtocolVersion(Proto(req.Proto)))
attrs = append(attrs, semconv.ServerAddress(req.Host))
attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))
e.semConvMetricRegistry.HTTPServerRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
}
}

View File

@ -5,19 +5,11 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"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/accesslog"
"github.com/traefik/traefik/v3/pkg/tracing"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/otel/attribute"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
)
func TestEntryPointMiddleware_tracing(t *testing.T) {
@ -77,7 +69,7 @@ func TestEntryPointMiddleware_tracing(t *testing.T) {
tracer := &mockTracer{}
handler := newEntryPoint(context.Background(), tracing.NewTracer(tracer, []string{"X-Foo"}, []string{"X-Bar"}, []string{"q"}), nil, test.entryPoint, next)
handler := newEntryPoint(context.Background(), tracing.NewTracer(tracer, []string{"X-Foo"}, []string{"X-Bar"}, []string{"q"}), test.entryPoint, next)
handler.ServeHTTP(rw, req)
for _, span := range tracer.spans {
@ -88,101 +80,6 @@ func TestEntryPointMiddleware_tracing(t *testing.T) {
}
}
func TestEntryPointMiddleware_metrics(t *testing.T) {
tests := []struct {
desc string
statusCode int
wantAttributes attribute.Set
}{
{
desc: "not found status",
statusCode: http.StatusNotFound,
wantAttributes: attribute.NewSet(
attribute.Key("error.type").String("404"),
attribute.Key("http.request.method").String("GET"),
attribute.Key("http.response.status_code").Int(404),
attribute.Key("network.protocol.name").String("http/1.1"),
attribute.Key("network.protocol.version").String("1.1"),
attribute.Key("server.address").String("www.test.com"),
attribute.Key("url.scheme").String("http"),
),
},
{
desc: "created status",
statusCode: http.StatusCreated,
wantAttributes: attribute.NewSet(
attribute.Key("http.request.method").String("GET"),
attribute.Key("http.response.status_code").Int(201),
attribute.Key("network.protocol.name").String("http/1.1"),
attribute.Key("network.protocol.version").String("1.1"),
attribute.Key("server.address").String("www.test.com"),
attribute.Key("url.scheme").String("http"),
),
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var cfg types.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)
rdr := sdkmetric.NewManualReader()
meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(rdr))
// force the meter provider with manual reader to collect metrics for the test.
metrics.SetMeterProvider(meterProvider)
semConvMetricRegistry, err := metrics.NewSemConvMetricRegistry(context.Background(), &cfg)
require.NoError(t, err)
require.NotNil(t, semConvMetricRegistry)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/search?q=Opentelemetry", nil)
rw := httptest.NewRecorder()
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "entrypoint-test")
req.Header.Set("X-Forwarded-Proto", "http")
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(test.statusCode)
})
handler := newEntryPoint(context.Background(), nil, semConvMetricRegistry, "test", next)
handler.ServeHTTP(rw, req)
got := metricdata.ResourceMetrics{}
err = rdr.Collect(context.Background(), &got)
require.NoError(t, err)
require.Len(t, got.ScopeMetrics, 1)
expected := metricdata.Metrics{
Name: "http.server.request.duration",
Description: "Duration of HTTP server requests.",
Unit: "s",
Data: metricdata.Histogram[float64]{
DataPoints: []metricdata.HistogramDataPoint[float64]{
{
Attributes: test.wantAttributes,
Count: 1,
Bounds: []float64{0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10},
BucketCounts: []uint64{0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
Min: metricdata.NewExtrema[float64](1),
Max: metricdata.NewExtrema[float64](1),
Sum: 1,
},
},
Temporality: metricdata.CumulativeTemporality,
},
}
metricdatatest.AssertEqual[metricdata.Metrics](t, expected, got.ScopeMetrics[0].Metrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
})
}
}
func TestEntryPointMiddleware_tracingInfoIntoLog(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/", http.NoBody)
req = req.WithContext(
@ -197,7 +94,7 @@ func TestEntryPointMiddleware_tracingInfoIntoLog(t *testing.T) {
tracer := &mockTracer{}
handler := newEntryPoint(context.Background(), tracing.NewTracer(tracer, []string{}, []string{}, []string{}), nil, "test", next)
handler := newEntryPoint(context.Background(), tracing.NewTracer(tracer, []string{}, []string{}, []string{}), "test", next)
handler.ServeHTTP(httptest.NewRecorder(), req)
expectedSpanCtx := tracer.spans[0].SpanContext()

View File

@ -8,6 +8,11 @@ import (
"go.opentelemetry.io/otel/trace"
)
type contextKey int
// DisableMetricsKey is a context key used to disable the metrics.
const DisableMetricsKey contextKey = iota
// 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 {

View File

@ -0,0 +1,81 @@
package observability
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/containous/alice"
"github.com/rs/zerolog/log"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/capture"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
const (
semConvServerMetricsTypeName = "SemConvServerMetrics"
)
type semConvServerMetrics struct {
next http.Handler
semConvMetricRegistry *metrics.SemConvMetricsRegistry
}
// SemConvServerMetricsHandler return the alice.Constructor for semantic conventions servers metrics.
func SemConvServerMetricsHandler(ctx context.Context, semConvMetricRegistry *metrics.SemConvMetricsRegistry) alice.Constructor {
return func(next http.Handler) (http.Handler, error) {
return newServerMetricsSemConv(ctx, semConvMetricRegistry, next), nil
}
}
// newServerMetricsSemConv creates a new semConv server metrics middleware for incoming requests.
func newServerMetricsSemConv(ctx context.Context, semConvMetricRegistry *metrics.SemConvMetricsRegistry, next http.Handler) http.Handler {
middlewares.GetLogger(ctx, "tracing", semConvServerMetricsTypeName).Debug().Msg("Creating middleware")
return &semConvServerMetrics{
semConvMetricRegistry: semConvMetricRegistry,
next: next,
}
}
func (e *semConvServerMetrics) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if e.semConvMetricRegistry == nil || e.semConvMetricRegistry.HTTPServerRequestDuration() == nil {
e.next.ServeHTTP(rw, req)
return
}
start := time.Now()
e.next.ServeHTTP(rw, req)
end := time.Now()
ctx := req.Context()
capt, err := capture.FromContext(ctx)
if err != nil {
log.Ctx(ctx).Error().Err(err).Str(logs.MiddlewareType, semConvServerMetricsTypeName).Msg("Could not get Capture")
return
}
var attrs []attribute.KeyValue
if capt.StatusCode() < 100 || capt.StatusCode() >= 600 {
attrs = append(attrs, attribute.Key("error.type").String(fmt.Sprintf("Invalid HTTP status code ; %d", capt.StatusCode())))
} else if capt.StatusCode() >= 400 {
attrs = append(attrs, attribute.Key("error.type").String(strconv.Itoa(capt.StatusCode())))
}
attrs = append(attrs, semconv.HTTPRequestMethodKey.String(req.Method))
attrs = append(attrs, semconv.HTTPResponseStatusCode(capt.StatusCode()))
attrs = append(attrs, semconv.NetworkProtocolName(strings.ToLower(req.Proto)))
attrs = append(attrs, semconv.NetworkProtocolVersion(Proto(req.Proto)))
attrs = append(attrs, semconv.ServerAddress(req.Host))
attrs = append(attrs, semconv.URLScheme(req.Header.Get("X-Forwarded-Proto")))
e.semConvMetricRegistry.HTTPServerRequestDuration().Record(req.Context(), end.Sub(start).Seconds(), metric.WithAttributes(attrs...))
}

View File

@ -0,0 +1,118 @@
package observability
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"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/capture"
"github.com/traefik/traefik/v3/pkg/types"
"go.opentelemetry.io/otel/attribute"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
)
func TestSemConvServerMetrics(t *testing.T) {
tests := []struct {
desc string
statusCode int
wantAttributes attribute.Set
}{
{
desc: "not found status",
statusCode: http.StatusNotFound,
wantAttributes: attribute.NewSet(
attribute.Key("error.type").String("404"),
attribute.Key("http.request.method").String("GET"),
attribute.Key("http.response.status_code").Int(404),
attribute.Key("network.protocol.name").String("http/1.1"),
attribute.Key("network.protocol.version").String("1.1"),
attribute.Key("server.address").String("www.test.com"),
attribute.Key("url.scheme").String("http"),
),
},
{
desc: "created status",
statusCode: http.StatusCreated,
wantAttributes: attribute.NewSet(
attribute.Key("http.request.method").String("GET"),
attribute.Key("http.response.status_code").Int(201),
attribute.Key("network.protocol.name").String("http/1.1"),
attribute.Key("network.protocol.version").String("1.1"),
attribute.Key("server.address").String("www.test.com"),
attribute.Key("url.scheme").String("http"),
),
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var cfg types.OTLP
(&cfg).SetDefaults()
cfg.AddRoutersLabels = true
cfg.PushInterval = ptypes.Duration(10 * time.Millisecond)
rdr := sdkmetric.NewManualReader()
meterProvider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(rdr))
// force the meter provider with manual reader to collect metrics for the test.
metrics.SetMeterProvider(meterProvider)
semConvMetricRegistry, err := metrics.NewSemConvMetricRegistry(context.Background(), &cfg)
require.NoError(t, err)
require.NotNil(t, semConvMetricRegistry)
req := httptest.NewRequest(http.MethodGet, "http://www.test.com/search?q=Opentelemetry", nil)
rw := httptest.NewRecorder()
req.RemoteAddr = "10.0.0.1:1234"
req.Header.Set("User-Agent", "entrypoint-test")
req.Header.Set("X-Forwarded-Proto", "http")
next := http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
rw.WriteHeader(test.statusCode)
})
handler := newServerMetricsSemConv(context.Background(), semConvMetricRegistry, next)
handler, err = capture.Wrap(handler)
require.NoError(t, err)
handler.ServeHTTP(rw, req)
got := metricdata.ResourceMetrics{}
err = rdr.Collect(context.Background(), &got)
require.NoError(t, err)
require.Len(t, got.ScopeMetrics, 1)
expected := metricdata.Metrics{
Name: "http.server.request.duration",
Description: "Duration of HTTP server requests.",
Unit: "s",
Data: metricdata.Histogram[float64]{
DataPoints: []metricdata.HistogramDataPoint[float64]{
{
Attributes: test.wantAttributes,
Count: 1,
Bounds: []float64{0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10},
BucketCounts: []uint64{0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
Min: metricdata.NewExtrema[float64](1),
Max: metricdata.NewExtrema[float64](1),
Sum: 1,
},
},
Temporality: metricdata.CumulativeTemporality,
},
}
metricdatatest.AssertEqual[metricdata.Metrics](t, expected, got.ScopeMetrics[0].Metrics[0], metricdatatest.IgnoreTimestamp(), metricdatatest.IgnoreValue())
})
}
}

View File

@ -12,6 +12,10 @@ spec:
- match: Host(`foo.com`) && PathPrefix(`/bar`)
kind: Rule
priority: 12
observability:
accessLogs: true
tracing: true
metrics: true
services:
- name: whoami
port: 80

View File

@ -114,12 +114,13 @@ func (p *Provider) loadIngressRouteConfiguration(ctx context.Context, client Cli
}
r := &dynamic.Router{
Middlewares: mds,
Priority: route.Priority,
RuleSyntax: route.Syntax,
EntryPoints: ingressRoute.Spec.EntryPoints,
Rule: route.Match,
Service: serviceName,
Middlewares: mds,
Priority: route.Priority,
RuleSyntax: route.Syntax,
EntryPoints: ingressRoute.Spec.EntryPoints,
Rule: route.Match,
Service: serviceName,
Observability: route.Observability,
}
if ingressRoute.Spec.TLS != nil {

View File

@ -1688,6 +1688,11 @@ func TestLoadIngressRoutes(t *testing.T) {
Service: "default-test-route-6b204d94623b3df4370c",
Rule: "Host(`foo.com`) && PathPrefix(`/bar`)",
Priority: 12,
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
Middlewares: map[string]*dynamic.Middleware{},

View File

@ -43,6 +43,9 @@ type Route struct {
// Middlewares defines the list of references to Middleware resources.
// More info: https://doc.traefik.io/traefik/v3.2/routing/providers/kubernetes-crd/#kind-middleware
Middlewares []MiddlewareRef `json:"middlewares,omitempty"`
// Observability defines the observability configuration for a router.
// More info: https://doc.traefik.io/traefik/v3.2/routing/routers/#observability
Observability *dynamic.RouterObservabilityConfig `json:"observability,omitempty"`
}
// TLS holds the TLS configuration.

View File

@ -1102,6 +1102,11 @@ func (in *Route) DeepCopyInto(out *Route) {
*out = make([]MiddlewareRef, len(*in))
copy(*out, *in)
}
if in.Observability != nil {
in, out := &in.Observability, &out.Observability
*out = new(dynamic.RouterObservabilityConfig)
(*in).DeepCopyInto(*out)
}
return
}

View File

@ -22,12 +22,13 @@ type RouterConfig struct {
// RouterIng is the router's configuration from annotations.
type RouterIng struct {
PathMatcher string `json:"pathMatcher,omitempty"`
EntryPoints []string `json:"entryPoints,omitempty"`
Middlewares []string `json:"middlewares,omitempty"`
Priority int `json:"priority,omitempty"`
RuleSyntax string `json:"ruleSyntax,omitempty"`
TLS *dynamic.RouterTLSConfig `json:"tls,omitempty" label:"allowEmpty"`
PathMatcher string `json:"pathMatcher,omitempty"`
EntryPoints []string `json:"entryPoints,omitempty"`
Middlewares []string `json:"middlewares,omitempty"`
Priority int `json:"priority,omitempty"`
RuleSyntax string `json:"ruleSyntax,omitempty"`
TLS *dynamic.RouterTLSConfig `json:"tls,omitempty" label:"allowEmpty"`
Observability *dynamic.RouterObservabilityConfig `json:"observability,omitempty" label:"allowEmpty"`
}
// SetDefaults sets the default values.

View File

@ -18,20 +18,23 @@ func Test_parseRouterConfig(t *testing.T) {
{
desc: "router annotations",
annotations: map[string]string{
"ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/router.pathmatcher": "foobar",
"traefik.ingress.kubernetes.io/router.entrypoints": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.middlewares": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.priority": "42",
"traefik.ingress.kubernetes.io/router.rulesyntax": "foobar",
"traefik.ingress.kubernetes.io/router.tls": "true",
"traefik.ingress.kubernetes.io/router.tls.certresolver": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.options": "foobar",
"ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/router.pathmatcher": "foobar",
"traefik.ingress.kubernetes.io/router.entrypoints": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.middlewares": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.priority": "42",
"traefik.ingress.kubernetes.io/router.rulesyntax": "foobar",
"traefik.ingress.kubernetes.io/router.tls": "true",
"traefik.ingress.kubernetes.io/router.tls.certresolver": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.options": "foobar",
"traefik.ingress.kubernetes.io/router.observability.accessLogs": "true",
"traefik.ingress.kubernetes.io/router.observability.metrics": "true",
"traefik.ingress.kubernetes.io/router.observability.tracing": "true",
},
expected: &RouterConfig{
Router: &RouterIng{
@ -54,6 +57,11 @@ func Test_parseRouterConfig(t *testing.T) {
},
Options: "foobar",
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
},
@ -182,35 +190,41 @@ func Test_convertAnnotations(t *testing.T) {
{
desc: "router annotations",
annotations: map[string]string{
"ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/router.pathmatcher": "foobar",
"traefik.ingress.kubernetes.io/router.entrypoints": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.middlewares": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.priority": "42",
"traefik.ingress.kubernetes.io/router.rulesyntax": "foobar",
"traefik.ingress.kubernetes.io/router.tls": "true",
"traefik.ingress.kubernetes.io/router.tls.certresolver": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.options": "foobar",
"ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/foo": "bar",
"traefik.ingress.kubernetes.io/router.pathmatcher": "foobar",
"traefik.ingress.kubernetes.io/router.entrypoints": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.middlewares": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.priority": "42",
"traefik.ingress.kubernetes.io/router.rulesyntax": "foobar",
"traefik.ingress.kubernetes.io/router.tls": "true",
"traefik.ingress.kubernetes.io/router.tls.certresolver": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.0.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.main": "foobar",
"traefik.ingress.kubernetes.io/router.tls.domains.1.sans": "foobar,foobar",
"traefik.ingress.kubernetes.io/router.tls.options": "foobar",
"traefik.ingress.kubernetes.io/router.observability.accessLogs": "true",
"traefik.ingress.kubernetes.io/router.observability.metrics": "true",
"traefik.ingress.kubernetes.io/router.observability.tracing": "true",
},
expected: map[string]string{
"traefik.foo": "bar",
"traefik.router.pathmatcher": "foobar",
"traefik.router.entrypoints": "foobar,foobar",
"traefik.router.middlewares": "foobar,foobar",
"traefik.router.priority": "42",
"traefik.router.rulesyntax": "foobar",
"traefik.router.tls": "true",
"traefik.router.tls.certresolver": "foobar",
"traefik.router.tls.domains[0].main": "foobar",
"traefik.router.tls.domains[0].sans": "foobar,foobar",
"traefik.router.tls.domains[1].main": "foobar",
"traefik.router.tls.domains[1].sans": "foobar,foobar",
"traefik.router.tls.options": "foobar",
"traefik.foo": "bar",
"traefik.router.pathmatcher": "foobar",
"traefik.router.entrypoints": "foobar,foobar",
"traefik.router.middlewares": "foobar,foobar",
"traefik.router.priority": "42",
"traefik.router.rulesyntax": "foobar",
"traefik.router.tls": "true",
"traefik.router.tls.certresolver": "foobar",
"traefik.router.tls.domains[0].main": "foobar",
"traefik.router.tls.domains[0].sans": "foobar,foobar",
"traefik.router.tls.domains[1].main": "foobar",
"traefik.router.tls.domains[1].sans": "foobar,foobar",
"traefik.router.tls.options": "foobar",
"traefik.router.observability.accessLogs": "true",
"traefik.router.observability.metrics": "true",
"traefik.router.observability.tracing": "true",
},
},
{

View File

@ -18,6 +18,9 @@ metadata:
traefik.ingress.kubernetes.io/router.tls.domains.1.main: example.com
traefik.ingress.kubernetes.io/router.tls.domains.1.sans: one.example.com,two.example.com
traefik.ingress.kubernetes.io/router.tls.options: foobar
traefik.ingress.kubernetes.io/router.observability.accesslogs: "true"
traefik.ingress.kubernetes.io/router.observability.metrics: "true"
traefik.ingress.kubernetes.io/router.observability.tracing: "true"
spec:
rules:

View File

@ -293,6 +293,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
rt.EntryPoints = rtConfig.Router.EntryPoints
rt.Middlewares = rtConfig.Router.Middlewares
rt.TLS = rtConfig.Router.TLS
rt.Observability = rtConfig.Router.Observability
}
p.applyRouterTransform(ctxIngress, rt, ingress)
@ -619,10 +620,8 @@ func (p *Provider) loadRouter(rule netv1.IngressRule, pa netv1.HTTPIngressPath,
rt.Priority = rtConfig.Router.Priority
rt.EntryPoints = rtConfig.Router.EntryPoints
rt.Middlewares = rtConfig.Router.Middlewares
if rtConfig.Router.TLS != nil {
rt.TLS = rtConfig.Router.TLS
}
rt.TLS = rtConfig.Router.TLS
rt.Observability = rtConfig.Router.Observability
}
var rules []string

View File

@ -115,6 +115,11 @@ func TestLoadConfigurationFromIngresses(t *testing.T) {
},
Options: "foobar",
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
Services: map[string]*dynamic.Service{

View File

@ -27,6 +27,11 @@
]
}
]
},
"observability": {
"accessLogs": false,
"tracing": false,
"metrics": false
}
}
}

View File

@ -231,6 +231,14 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
Middlewares: ep.HTTP.Middlewares,
}
if ep.Observability != nil {
m.Observability = dynamic.RouterObservabilityConfig{
AccessLogs: &ep.Observability.AccessLogs,
Tracing: &ep.Observability.Tracing,
Metrics: &ep.Observability.Metrics,
}
}
if ep.HTTP.TLS != nil {
m.TLS = &dynamic.RouterTLSConfig{
Options: ep.HTTP.TLS.Options,

View File

@ -184,6 +184,11 @@ func Test_createConfiguration(t *testing.T) {
},
},
},
Observability: &static.ObservabilityConfig{
AccessLogs: false,
Tracing: false,
Metrics: false,
},
},
},
},

View File

@ -68,7 +68,7 @@ func (t *wrapper) RoundTrip(req *http.Request) (*http.Response, error) {
span.End(trace.WithTimestamp(end))
}
if t.semConvMetricRegistry != nil && t.semConvMetricRegistry.HTTPClientRequestDuration() != nil {
if req.Context().Value(observability.DisableMetricsKey) == nil && t.semConvMetricRegistry != nil && t.semConvMetricRegistry.HTTPClientRequestDuration() != nil {
var attrs []attribute.KeyValue
if statusCode < 100 || statusCode >= 600 {

View File

@ -57,6 +57,11 @@ func init() {
},
},
},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
Services: map[string]*dynamic.Service{

View File

@ -22,6 +22,11 @@
]
}
]
},
"observability": {
"accessLogs": true,
"tracing": true,
"metrics": true
}
}
},
@ -327,7 +332,8 @@
]
}
]
}
},
"observability": {}
}
},
"serversTransports": {

View File

@ -22,6 +22,11 @@
]
}
]
},
"observability": {
"accessLogs": true,
"tracing": true,
"metrics": true
}
}
},
@ -330,7 +335,8 @@
]
}
]
}
},
"observability": {}
}
},
"serversTransports": {

View File

@ -178,6 +178,22 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
cp.Middlewares = append(m.Middlewares, cp.Middlewares...)
if cp.Observability == nil {
cp.Observability = &dynamic.RouterObservabilityConfig{}
}
if cp.Observability.AccessLogs == nil {
cp.Observability.AccessLogs = m.Observability.AccessLogs
}
if cp.Observability.Tracing == nil {
cp.Observability.Tracing = m.Observability.Tracing
}
if cp.Observability.Metrics == nil {
cp.Observability.Metrics = m.Observability.Metrics
}
rtName := name
if len(eps) > 1 {
rtName = epName + "-" + name

View File

@ -9,6 +9,8 @@ import (
"github.com/traefik/traefik/v3/pkg/tls"
)
func pointer[T any](v T) *T { return &v }
func Test_mergeConfiguration(t *testing.T) {
testCases := []struct {
desc string
@ -558,9 +560,10 @@ func Test_applyModel(t *testing.T) {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"test": {
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{},
},
},
Middlewares: make(map[string]*dynamic.Middleware),
@ -574,6 +577,60 @@ func Test_applyModel(t *testing.T) {
},
},
},
{
desc: "with model, one entry point with observability",
input: dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"test": {
EntryPoints: []string{"websecure"},
},
},
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
Models: map[string]*dynamic.Model{
"websecure@internal": {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
},
},
expected: dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"test": {
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
Middlewares: make(map[string]*dynamic.Middleware),
Services: make(map[string]*dynamic.Service),
Models: map[string]*dynamic.Model{
"websecure@internal": {
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: dynamic.RouterObservabilityConfig{
AccessLogs: pointer(true),
Tracing: pointer(true),
Metrics: pointer(true),
},
},
},
},
},
},
{
desc: "with model, one entry point, and router with tls",
input: dynamic.Configuration{
@ -601,6 +658,11 @@ func Test_applyModel(t *testing.T) {
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{CertResolver: "router"},
Observability: &dynamic.RouterObservabilityConfig{
AccessLogs: nil,
Tracing: nil,
Metrics: nil,
},
},
},
Middlewares: make(map[string]*dynamic.Middleware),
@ -640,9 +702,10 @@ func Test_applyModel(t *testing.T) {
EntryPoints: []string{"web"},
},
"websecure-test": {
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
EntryPoints: []string{"websecure"},
Middlewares: []string{"test"},
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{},
},
},
Middlewares: make(map[string]*dynamic.Middleware),

View File

@ -8,12 +8,13 @@ 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/static"
"github.com/traefik/traefik/v3/pkg/logs"
"github.com/traefik/traefik/v3/pkg/metrics"
"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"
mmetrics "github.com/traefik/traefik/v3/pkg/middlewares/metrics"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
"github.com/traefik/traefik/v3/pkg/tracing"
)
@ -41,7 +42,7 @@ 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) alice.Chain {
func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName string, resourceName string, observabilityConfig *dynamic.RouterObservabilityConfig) alice.Chain {
chain := alice.New()
if o == nil {
@ -49,62 +50,101 @@ func (o *ObservabilityMgr) BuildEPChain(ctx context.Context, entryPointName stri
}
if o.accessLoggerMiddleware != nil || o.metricsRegistry != nil && (o.metricsRegistry.IsEpEnabled() || o.metricsRegistry.IsRouterEnabled() || o.metricsRegistry.IsSvcEnabled()) {
if o.ShouldAddAccessLogs(resourceName) || o.ShouldAddMetrics(resourceName) {
if o.ShouldAddAccessLogs(resourceName, observabilityConfig) || o.ShouldAddMetrics(resourceName, observabilityConfig) {
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)) || (o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName)) {
chain = chain.Append(observability.WrapEntryPointHandler(ctx, o.tracer, o.semConvMetricRegistry, entryPointName))
if o.tracer != nil && o.ShouldAddTracing(resourceName, observabilityConfig) {
chain = chain.Append(observability.EntryPointHandler(ctx, o.tracer, entryPointName))
}
if o.accessLoggerMiddleware != nil && o.ShouldAddAccessLogs(resourceName) {
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
})
}
if o.metricsRegistry != nil && o.metricsRegistry.IsEpEnabled() && o.ShouldAddMetrics(resourceName) {
metricsHandler := metricsMiddle.WrapEntryPointHandler(ctx, o.metricsRegistry, entryPointName)
// Semantic convention server metrics handler.
if o.semConvMetricRegistry != nil && o.ShouldAddMetrics(resourceName, observabilityConfig) {
chain = chain.Append(observability.SemConvServerMetricsHandler(ctx, o.semConvMetricRegistry))
}
if o.tracer != nil && o.ShouldAddTracing(resourceName) {
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
})
}
return chain
}
// ShouldAddAccessLogs returns whether the access logs should be enabled for the given resource.
func (o *ObservabilityMgr) ShouldAddAccessLogs(resourceName string) bool {
// 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
}
return o.config.AccessLog != nil && (o.config.AccessLog.AddInternals || !strings.HasSuffix(resourceName, "@internal"))
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.
func (o *ObservabilityMgr) ShouldAddMetrics(resourceName string) bool {
// 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
}
return o.config.Metrics != nil && (o.config.Metrics.AddInternals || !strings.HasSuffix(resourceName, "@internal"))
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 resource.
func (o *ObservabilityMgr) ShouldAddTracing(resourceName string) bool {
// 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
}
return o.config.Tracing != nil && (o.config.Tracing.AddInternals || !strings.HasSuffix(resourceName, "@internal"))
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.

View File

@ -91,12 +91,12 @@ func (m *Manager) BuildHandlers(rootCtx context.Context, entryPoints []string, t
continue
}
handler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "").Then(BuildDefaultHTTPRouter())
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "", nil).Then(BuildDefaultHTTPRouter())
if err != nil {
logger.Error().Err(err).Send()
continue
}
entryPointHandlers[entryPointName] = handler
entryPointHandlers[entryPointName] = defaultHandler
}
return entryPointHandlers
@ -108,7 +108,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
return nil, err
}
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "defaultHandler").Then(http.NotFoundHandler())
defaultHandler, err := m.observabilityMgr.BuildEPChain(ctx, entryPointName, "", nil).Then(http.NotFoundHandler())
if err != nil {
return nil, err
}
@ -137,7 +137,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
continue
}
observabilityChain := m.observabilityMgr.BuildEPChain(ctx, entryPointName, routerConfig.Service)
observabilityChain := m.observabilityMgr.BuildEPChain(ctx, entryPointName, routerConfig.Service, routerConfig.Observability)
handler, err = observabilityChain.Then(handler)
if err != nil {
routerConfig.AddError(err, true)
@ -182,7 +182,7 @@ func (m *Manager) buildRouterHandler(ctx context.Context, routerName string, rou
}
// Prevents from enabling observability for internal resources.
if !m.observabilityMgr.ShouldAddAccessLogs(provider.GetQualifiedName(ctx, routerConfig.Service)) {
if !m.observabilityMgr.ShouldAddAccessLogs(provider.GetQualifiedName(ctx, routerConfig.Service), routerConfig.Observability) {
m.routerHandlers[routerName] = handler
return m.routerHandlers[routerName], nil
}
@ -221,12 +221,12 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
chain := alice.New()
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsRouterEnabled() &&
m.observabilityMgr.ShouldAddMetrics(provider.GetQualifiedName(ctx, router.Service)) {
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)) {
if !m.observabilityMgr.ShouldAddTracing(provider.GetQualifiedName(ctx, router.Service), router.Observability) {
return chain.Extend(*mHandler).Then(sHandler)
}

View File

@ -356,7 +356,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
qualifiedSvcName := provider.GetQualifiedName(ctx, serviceName)
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName)
shouldObserve := m.observabilityMgr.ShouldAddTracing(qualifiedSvcName, nil) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil)
proxy, err := m.proxyBuilder.Build(service.ServersTransport, target, shouldObserve, passHostHeader, server.PreservePath, flushInterval)
if err != nil {
return nil, fmt.Errorf("error building proxy for server URL %s: %w", server.URL, err)
@ -364,14 +364,14 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
// Prevents from enabling observability for internal resources.
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName) {
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)
}
if m.observabilityMgr.MetricsRegistry() != nil && m.observabilityMgr.MetricsRegistry().IsSvcEnabled() &&
m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName) {
m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName, nil) {
metricsHandler := metricsMiddle.WrapServiceHandler(ctx, m.observabilityMgr.MetricsRegistry(), serviceName)
proxy, err = alice.New().
@ -382,11 +382,11 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
}
}
if m.observabilityMgr.ShouldAddTracing(qualifiedSvcName) {
if m.observabilityMgr.ShouldAddTracing(qualifiedSvcName, nil) {
proxy = observability.NewService(ctx, serviceName, proxy)
}
if m.observabilityMgr.ShouldAddAccessLogs(qualifiedSvcName) || m.observabilityMgr.ShouldAddMetrics(qualifiedSvcName) {
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.