From b1934231ca1801ebc4466823e864fccd10aaffb7 Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 12 Dec 2024 09:52:07 +0100 Subject: [PATCH] Manage observability at entrypoint and router level Co-authored-by: Kevin Pollet --- docs/content/observability/metrics/datadog.md | 1 + docs/content/observability/overview.md | 76 ++++++++++- .../dynamic-configuration/docker-labels.yml | 6 + .../reference/dynamic-configuration/file.toml | 8 ++ .../reference/dynamic-configuration/file.yaml | 8 ++ .../kubernetes-crd-definition-v1.yml | 12 ++ .../reference/dynamic-configuration/kv-ref.md | 6 + .../traefik.io_ingressroutes.yaml | 12 ++ .../reference/static-configuration/cli-ref.md | 9 ++ .../reference/static-configuration/env-ref.md | 9 ++ .../reference/static-configuration/file.toml | 4 + .../reference/static-configuration/file.yaml | 4 + docs/content/routing/entrypoints.md | 104 ++++++++++++++- .../routing/providers/consul-catalog.md | 24 ++++ docs/content/routing/providers/docker.md | 24 ++++ docs/content/routing/providers/ecs.md | 24 ++++ .../routing/providers/kubernetes-crd.md | 68 +++++----- .../routing/providers/kubernetes-ingress.md | 24 ++++ docs/content/routing/providers/kv.md | 24 ++++ docs/content/routing/providers/marathon.md | 0 docs/content/routing/providers/nomad.md | 24 ++++ .../routing/providers/service-by-label.md | 3 +- docs/content/routing/providers/swarm.md | 24 ++++ docs/content/routing/routers/index.md | 111 ++++++++++++++++ integration/fixtures/k8s/01-traefik-crd.yml | 12 ++ pkg/config/dynamic/http_config.go | 35 ++++-- pkg/config/dynamic/zz_generated.deepcopy.go | 37 ++++++ pkg/config/label/label_test.go | 38 ++++-- pkg/config/static/entrypoints.go | 17 +++ pkg/config/static/static_config_test.go | 20 +++ pkg/middlewares/metrics/metrics.go | 5 + pkg/middlewares/observability/entrypoint.go | 49 ++------ .../observability/entrypoint_test.go | 107 +--------------- .../observability/observability.go | 5 + pkg/middlewares/observability/semconv.go | 81 ++++++++++++ pkg/middlewares/observability/semconv_test.go | 118 ++++++++++++++++++ .../kubernetes/crd/fixtures/simple.yml | 4 + .../kubernetes/crd/kubernetes_http.go | 13 +- .../kubernetes/crd/kubernetes_test.go | 5 + .../crd/traefikio/v1alpha1/ingressroute.go | 3 + .../v1alpha1/zz_generated.deepcopy.go | 5 + .../kubernetes/ingress/annotations.go | 13 +- .../kubernetes/ingress/annotations_test.go | 96 ++++++++------ .../fixtures/Ingress-with-annotations.yml | 3 + pkg/provider/kubernetes/ingress/kubernetes.go | 7 +- .../kubernetes/ingress/kubernetes_test.go | 5 + pkg/provider/traefik/fixtures/models.json | 5 + pkg/provider/traefik/internal.go | 8 ++ pkg/provider/traefik/internal_test.go | 5 + pkg/proxy/httputil/observability.go | 2 +- pkg/redactor/redactor_config_test.go | 5 + .../testdata/anonymized-dynamic-config.json | 8 +- .../testdata/secured-dynamic-config.json | 8 +- pkg/server/aggregator.go | 16 +++ pkg/server/aggregator_test.go | 75 ++++++++++- pkg/server/middleware/observability.go | 76 ++++++++--- pkg/server/router/router.go | 14 +-- pkg/server/service/service.go | 10 +- 58 files changed, 1216 insertions(+), 303 deletions(-) delete mode 100644 docs/content/routing/providers/marathon.md create mode 100644 pkg/middlewares/observability/semconv.go create mode 100644 pkg/middlewares/observability/semconv_test.go diff --git a/docs/content/observability/metrics/datadog.md b/docs/content/observability/metrics/datadog.md index 61c64f4c9..fa3ac4592 100644 --- a/docs/content/observability/metrics/datadog.md +++ b/docs/content/observability/metrics/datadog.md @@ -68,6 +68,7 @@ metrics: ```bash tab="CLI" --metrics.datadog.addEntryPointsLabels=true ``` + #### `addRoutersLabels` _Optional, Default=false_ diff --git a/docs/content/observability/overview.md b/docs/content/observability/overview.md index f5f46bbf3..bcd4385dd 100644 --- a/docs/content/observability/overview.md +++ b/docs/content/observability/overview.md @@ -5,16 +5,80 @@ description: "Traefik provides Logs, Access Logs, Metrics and Tracing. Read the # Overview -Traefik's Observability system -{: .subtitle } +Traefikā€™s 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. diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index b126ab96c..63b86f2f0 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -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" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index e1a93d65f..0a0c6ec25 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -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] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 4f2ae185d..05908bb18 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -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: 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 8ba8377e1..86ccec173 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -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. diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 21b23ecbe..46d27bea8 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -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` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 07a5ead8b..d0f042d91 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -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. diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 0c65188fe..7feb19120 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -264,6 +264,15 @@ HTTP/3 configuration. (Default: ```false```) `--entrypoints..http3.advertisedport`: UDP port to advertise, on which HTTP/3 is available. (Default: ```0```) +`--entrypoints..observability.accesslogs`: + (Default: ```true```) + +`--entrypoints..observability.metrics`: + (Default: ```true```) + +`--entrypoints..observability.tracing`: + (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 056f9b29f..5a82b515e 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -264,6 +264,15 @@ Subject alternative names. `TRAEFIK_ENTRYPOINTS__HTTP_TLS_OPTIONS`: Default TLS options for the routers linked to the entry point. +`TRAEFIK_ENTRYPOINTS__OBSERVABILITY_ACCESSLOGS`: + (Default: ```true```) + +`TRAEFIK_ENTRYPOINTS__OBSERVABILITY_METRICS`: + (Default: ```true```) + +`TRAEFIK_ENTRYPOINTS__OBSERVABILITY_TRACING`: + (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 1987627ec..25653d0ce 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -77,6 +77,10 @@ advertisedPort = 42 [entryPoints.EntryPoint0.udp] timeout = "42s" + [entryPoints.EntryPoint0.observability] + accessLogs = true + tracing = true + metrics = true [providers] providersThrottleDuration = "42s" diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index 2bebf7017..d40decc22 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -91,6 +91,10 @@ entryPoints: advertisedPort: 42 udp: timeout: 42s + observability: + accessLogs: true + tracing: true + metrics: true providers: providersThrottleDuration: 42s docker: diff --git a/docs/content/routing/entrypoints.md b/docs/content/routing/entrypoints.md index ce459375d..aefb7ac47 100644 --- a/docs/content/routing/entrypoints.md +++ b/docs/content/routing/entrypoints.md @@ -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!} diff --git a/docs/content/routing/providers/consul-catalog.md b/docs/content/routing/providers/consul-catalog.md index a2b0a7878..b3c918892 100644 --- a/docs/content/routing/providers/consul-catalog.md +++ b/docs/content/routing/providers/consul-catalog.md @@ -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..observability.accesslogs`" + + See accesslogs [option](../routers/index.md#accesslogs) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.accesslogs=true + ``` + +??? info "`traefik.http.routers..observability.metrics`" + + See metrics [option](../routers/index.md#metrics) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.metrics=true + ``` + +??? info "`traefik.http.routers..observability.tracing`" + + See tracing [option](../routers/index.md#tracing) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.tracing=true + ``` + ??? info "`traefik.http.routers..priority`" See [priority](../routers/index.md#priority) for more information. diff --git a/docs/content/routing/providers/docker.md b/docs/content/routing/providers/docker.md index 84c3af25d..43d785010 100644 --- a/docs/content/routing/providers/docker.md +++ b/docs/content/routing/providers/docker.md @@ -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..observability.accesslogs`" + + See accesslogs [option](../routers/index.md#accesslogs) for more information. + + ```yaml + - "traefik.http.routers.myrouter.observability.accesslogs=true" + ``` + +??? info "`traefik.http.routers..observability.metrics`" + + See metrics [option](../routers/index.md#metrics) for more information. + + ```yaml + - "traefik.http.routers.myrouter.observability.metrics=true" + ``` + +??? info "`traefik.http.routers..observability.tracing`" + + See tracing [option](../routers/index.md#tracing) for more information. + + ```yaml + - "traefik.http.routers.myrouter.observability.tracing=true" + ``` + ??? info "`traefik.http.routers..priority`" See [priority](../routers/index.md#priority) for more information. diff --git a/docs/content/routing/providers/ecs.md b/docs/content/routing/providers/ecs.md index 30c82fc9b..35ba6e09c 100644 --- a/docs/content/routing/providers/ecs.md +++ b/docs/content/routing/providers/ecs.md @@ -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..observability.accesslogs`" + + See accesslogs [option](../routers/index.md#accesslogs) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.accesslogs=true + ``` + +??? info "`traefik.http.routers..observability.metrics`" + + See metrics [option](../routers/index.md#metrics) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.metrics=true + ``` + +??? info "`traefik.http.routers..observability.tracing`" + + See tracing [option](../routers/index.md#tracing) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.tracing=true + ``` + ??? info "`traefik.http.routers..priority`" See [priority](../routers/index.md#priority) for more information. diff --git a/docs/content/routing/providers/kubernetes-crd.md b/docs/content/routing/providers/kubernetes-crd.md index edd3e4a3b..85dddccae 100644 --- a/docs/content/routing/providers/kubernetes-crd.md +++ b/docs/content/routing/providers/kubernetes-crd.md @@ -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" diff --git a/docs/content/routing/providers/kubernetes-ingress.md b/docs/content/routing/providers/kubernetes-ingress.md index 39993b60a..b8a4eaf9a 100644 --- a/docs/content/routing/providers/kubernetes-ingress.md +++ b/docs/content/routing/providers/kubernetes-ingress.md @@ -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`" diff --git a/docs/content/routing/providers/kv.md b/docs/content/routing/providers/kv.md index ba440db3f..86f70bc14 100644 --- a/docs/content/routing/providers/kv.md +++ b/docs/content/routing/providers/kv.md @@ -95,6 +95,30 @@ A Story of key & values |---------------------------------------------|----------| | `traefik/http/routers/myrouter/tls/options` | `foobar` | +??? info "`traefik/http/routers//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//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//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//priority`" See [priority](../routers/index.md#priority) for more information. diff --git a/docs/content/routing/providers/marathon.md b/docs/content/routing/providers/marathon.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/content/routing/providers/nomad.md b/docs/content/routing/providers/nomad.md index 2fbdd8a4e..476091201 100644 --- a/docs/content/routing/providers/nomad.md +++ b/docs/content/routing/providers/nomad.md @@ -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..observability.accesslogs`" + + See accesslogs [option](../routers/index.md#accesslogs) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.accesslogs=true + ``` + +??? info "`traefik.http.routers..observability.metrics`" + + See metrics [option](../routers/index.md#metrics) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.metrics=true + ``` + +??? info "`traefik.http.routers..observability.tracing`" + + See tracing [option](../routers/index.md#tracing) for more information. + + ```yaml + traefik.http.routers.myrouter.observability.tracing=true + ``` + ??? info "`traefik.http.routers..priority`" See [priority](../routers/index.md#priority) for more information. diff --git a/docs/content/routing/providers/service-by-label.md b/docs/content/routing/providers/service-by-label.md index 611f1ad2e..47da39538 100644 --- a/docs/content/routing/providers/service-by-label.md +++ b/docs/content/routing/providers/service-by-label.md @@ -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 "" diff --git a/docs/content/routing/providers/swarm.md b/docs/content/routing/providers/swarm.md index e6968b917..39c46235f 100644 --- a/docs/content/routing/providers/swarm.md +++ b/docs/content/routing/providers/swarm.md @@ -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..observability.accesslogs`" + + See accesslogs [option](../routers/index.md#accesslogs) for more information. + + ```yaml + - "traefik.http.routers.myrouter.observability.accesslogs=true" + ``` + +??? info "`traefik.http.routers..observability.metrics`" + + See metrics [option](../routers/index.md#metrics) for more information. + + ```yaml + - "traefik.http.routers.myrouter.observability.metrics=true" + ``` + +??? info "`traefik.http.routers..observability.tracing`" + + See tracing [option](../routers/index.md#tracing) for more information. + + ```yaml + - "traefik.http.routers.myrouter.observability.tracing=true" + ``` + ??? info "`traefik.http.routers..priority`" See [priority](../routers/index.md#priority) for more information. diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 7e93c79c3..ef61d56b1 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -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" diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 8ba8377e1..86ccec173 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -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. diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 7655036b0..18552294d 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -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"` diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 792ff4805..01c464096 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 7f5e9909b..553689adc 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -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", diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go index 6477da7e2..f443abe37 100644 --- a/pkg/config/static/entrypoints.go +++ b/pkg/config/static/entrypoints.go @@ -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 +} diff --git a/pkg/config/static/static_config_test.go b/pkg/config/static/static_config_test.go index 67c643a32..78633bc2f 100644 --- a/pkg/config/static/static_config_test.go +++ b/pkg/config/static/static_config_test.go @@ -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{ diff --git a/pkg/middlewares/metrics/metrics.go b/pkg/middlewares/metrics/metrics.go index 1bbe4798d..e8a1c6dba 100644 --- a/pkg/middlewares/metrics/metrics.go +++ b/pkg/middlewares/metrics/metrics.go @@ -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 diff --git a/pkg/middlewares/observability/entrypoint.go b/pkg/middlewares/observability/entrypoint.go index 5d1d1b877..8b356b03b 100644 --- a/pkg/middlewares/observability/entrypoint.go +++ b/pkg/middlewares/observability/entrypoint.go @@ -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...)) - } } diff --git a/pkg/middlewares/observability/entrypoint_test.go b/pkg/middlewares/observability/entrypoint_test.go index 3e7a90870..0b94f5fd5 100644 --- a/pkg/middlewares/observability/entrypoint_test.go +++ b/pkg/middlewares/observability/entrypoint_test.go @@ -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() diff --git a/pkg/middlewares/observability/observability.go b/pkg/middlewares/observability/observability.go index 54f243186..1ee9f3b99 100644 --- a/pkg/middlewares/observability/observability.go +++ b/pkg/middlewares/observability/observability.go @@ -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 { diff --git a/pkg/middlewares/observability/semconv.go b/pkg/middlewares/observability/semconv.go new file mode 100644 index 000000000..51f4480b5 --- /dev/null +++ b/pkg/middlewares/observability/semconv.go @@ -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...)) +} diff --git a/pkg/middlewares/observability/semconv_test.go b/pkg/middlewares/observability/semconv_test.go new file mode 100644 index 000000000..08846c4d7 --- /dev/null +++ b/pkg/middlewares/observability/semconv_test.go @@ -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()) + }) + } +} diff --git a/pkg/provider/kubernetes/crd/fixtures/simple.yml b/pkg/provider/kubernetes/crd/fixtures/simple.yml index 36d431338..3465c439a 100644 --- a/pkg/provider/kubernetes/crd/fixtures/simple.yml +++ b/pkg/provider/kubernetes/crd/fixtures/simple.yml @@ -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 diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index fb972a6c0..5c548a8ed 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -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 { diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index 29856eef1..a7ceb391e 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -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{}, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index 7e686562e..3a46caf9f 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -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. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index 466cc7577..45287c4c9 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -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 } diff --git a/pkg/provider/kubernetes/ingress/annotations.go b/pkg/provider/kubernetes/ingress/annotations.go index 144dd2a46..fe7f52d52 100644 --- a/pkg/provider/kubernetes/ingress/annotations.go +++ b/pkg/provider/kubernetes/ingress/annotations.go @@ -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. diff --git a/pkg/provider/kubernetes/ingress/annotations_test.go b/pkg/provider/kubernetes/ingress/annotations_test.go index 61c93061f..8f011e416 100644 --- a/pkg/provider/kubernetes/ingress/annotations_test.go +++ b/pkg/provider/kubernetes/ingress/annotations_test.go @@ -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", }, }, { diff --git a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml index 3a57a6345..e910efefa 100644 --- a/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml +++ b/pkg/provider/kubernetes/ingress/fixtures/Ingress-with-annotations.yml @@ -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: diff --git a/pkg/provider/kubernetes/ingress/kubernetes.go b/pkg/provider/kubernetes/ingress/kubernetes.go index ad53bb5eb..e1fce4e1f 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes.go +++ b/pkg/provider/kubernetes/ingress/kubernetes.go @@ -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 diff --git a/pkg/provider/kubernetes/ingress/kubernetes_test.go b/pkg/provider/kubernetes/ingress/kubernetes_test.go index ff54a3922..bbfeea77f 100644 --- a/pkg/provider/kubernetes/ingress/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress/kubernetes_test.go @@ -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{ diff --git a/pkg/provider/traefik/fixtures/models.json b/pkg/provider/traefik/fixtures/models.json index 005b6bf9a..65a6c88b0 100644 --- a/pkg/provider/traefik/fixtures/models.json +++ b/pkg/provider/traefik/fixtures/models.json @@ -27,6 +27,11 @@ ] } ] + }, + "observability": { + "accessLogs": false, + "tracing": false, + "metrics": false } } } diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go index 917c3d44d..e65ab64dc 100644 --- a/pkg/provider/traefik/internal.go +++ b/pkg/provider/traefik/internal.go @@ -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, diff --git a/pkg/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go index c8d64f6be..ed7197f82 100644 --- a/pkg/provider/traefik/internal_test.go +++ b/pkg/provider/traefik/internal_test.go @@ -184,6 +184,11 @@ func Test_createConfiguration(t *testing.T) { }, }, }, + Observability: &static.ObservabilityConfig{ + AccessLogs: false, + Tracing: false, + Metrics: false, + }, }, }, }, diff --git a/pkg/proxy/httputil/observability.go b/pkg/proxy/httputil/observability.go index 9240f5f7e..8fa3382e3 100644 --- a/pkg/proxy/httputil/observability.go +++ b/pkg/proxy/httputil/observability.go @@ -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 { diff --git a/pkg/redactor/redactor_config_test.go b/pkg/redactor/redactor_config_test.go index f26acd329..b086ccd6a 100644 --- a/pkg/redactor/redactor_config_test.go +++ b/pkg/redactor/redactor_config_test.go @@ -57,6 +57,11 @@ func init() { }, }, }, + Observability: &dynamic.RouterObservabilityConfig{ + AccessLogs: pointer(true), + Tracing: pointer(true), + Metrics: pointer(true), + }, }, }, Services: map[string]*dynamic.Service{ diff --git a/pkg/redactor/testdata/anonymized-dynamic-config.json b/pkg/redactor/testdata/anonymized-dynamic-config.json index ed3c07c86..4b71f1c76 100644 --- a/pkg/redactor/testdata/anonymized-dynamic-config.json +++ b/pkg/redactor/testdata/anonymized-dynamic-config.json @@ -22,6 +22,11 @@ ] } ] + }, + "observability": { + "accessLogs": true, + "tracing": true, + "metrics": true } } }, @@ -327,7 +332,8 @@ ] } ] - } + }, + "observability": {} } }, "serversTransports": { diff --git a/pkg/redactor/testdata/secured-dynamic-config.json b/pkg/redactor/testdata/secured-dynamic-config.json index 75c70ae25..c9674639b 100644 --- a/pkg/redactor/testdata/secured-dynamic-config.json +++ b/pkg/redactor/testdata/secured-dynamic-config.json @@ -22,6 +22,11 @@ ] } ] + }, + "observability": { + "accessLogs": true, + "tracing": true, + "metrics": true } } }, @@ -330,7 +335,8 @@ ] } ] - } + }, + "observability": {} } }, "serversTransports": { diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go index f015cb43d..0a849e3d8 100644 --- a/pkg/server/aggregator.go +++ b/pkg/server/aggregator.go @@ -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 diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go index b70d261ae..50a400d2a 100644 --- a/pkg/server/aggregator_test.go +++ b/pkg/server/aggregator_test.go @@ -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), diff --git a/pkg/server/middleware/observability.go b/pkg/server/middleware/observability.go index 902938acb..566d8522c 100644 --- a/pkg/server/middleware/observability.go +++ b/pkg/server/middleware/observability.go @@ -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. diff --git a/pkg/server/router/router.go b/pkg/server/router/router.go index e3af98f33..fcab90a94 100644 --- a/pkg/server/router/router.go +++ b/pkg/server/router/router.go @@ -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) } diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 602c7bd45..a4c5135e4 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -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.