Repository: traefik/traefik (language: go)

Issue:
Routers API endpoint repeats entries with multiple entrypoints on v3.3.2
### Welcome!

- [x] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [x] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

I have upgraded the docker image from v3.3.1 to v3.3.2.

### What did you see instead?

After the upgrade, the `/api/http/routers` endpoint prints routers differently, no longer printing entrypoints in an array but repeating them.

Before the upgrade (v3.3.1) the output would produce data like this:

```
curl -s "https://traefik.example.org/api/http/routers"  | jq ".[] | [.name, .using]" -c
["acme-http@internal",["web"]]
["dashboard@docker",["metrics","web","websecure"]]
["whoami@docker",["metrics","web","websecure"]]
```

After the upgrade (v3.3.2), it becomes this:

```
curl -s "https://traefik.example.org/api/http/routers"  | jq ".[] | [.name, .using]" -c
["acme-http@internal",["web"]]
["metrics-dashboard@docker",["metrics"]]
["metrics-whoami@docker",["metrics"]]
["web-dashboard@docker",["web"]]
["web-whoami@docker",["web"]]
["websecure-dashboard@docker",["websecure"]]
["websecure-whoami@docker",["websecure"]]
```

### What version of Traefik are you using?

```
% docker run traefik:v3.3.2 version
Version:      3.3.2
Codename:     saintnectaire
Go version:   go1.23.4
Built:        2025-01-14T15:52:24Z
OS/Arch:      linux/amd64
```

### What is your environment & configuration?

Running traefik on Docker using Docker compose. I see the same behavior on multiple installations running Ubuntu 24.04.1 and Debian 12.

### If applicable, please paste the log output in DEBUG level

_No response_


Discussion / hints:
I've made a sample `docker-compose.yaml` file to reproduce the issue, as well as the full API output and diff running the two versions.

<details>
<summary>docker-compose.yaml</summary>

```
# cat docker-compose.yml
---
services:
  traefik:
    image: traefik:v3.3.1
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entryPoints.web.address=:8090"
      - "--entryPoints.web2.address=:8091"
    labels:
      traefik.enable: true
      traefik.http.routers.dashboard.entrypoints: web
      traefik.http.routers.dashboard.service: "api@internal"
      traefik.http.routers.dashboard.rule: "(PathPrefix(`/dashboard`) || PathPrefix(`/api`))"
    ports:
      - 8090:8090
      - 8091:8091
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
  whoami:
    image: traefik/whoami
    labels:
      traefik.enable: true
      traefik.http.routers.whoami.rule: "Path(`/`)"
```
</details>

<details>
<summary>Using `traefik:v3.3.1` docker image</summary>

```
% curl -s "http://<IP>:8090/api/http/routers" | jq .
[
  {
    "entryPoints": [
      "traefik"
    ],
    "service": "api@internal",
    "rule": "PathPrefix(`/api`)",
    "ruleSyntax": "v3",
    "priority": 9223372036854775806,
    "status": "enabled",
    "using": [
      "traefik"
    ],
    "name": "api@internal",
    "provider": "internal"
  },
  {
    "entryPoints": [
      "web"
    ],
    "service": "api@internal",
    "rule": "(PathPrefix(`/dashboard`) || PathPrefix(`/api`))",
    "priority": 48,
    "status": "enabled",
    "using": [
      "web"
    ],
    "name": "dashboard@docker",
    "provider": "docker"
  },
  {
    "entryPoints": [
      "traefik"
    ],
    "middlewares": [
      "dashboard_redirect@internal",
      "dashboard_stripprefix@internal"
    ],
    "service": "dashboard@internal",
    "rule": "PathPrefix(`/`)",
    "ruleSyntax": "v3",
    "priority": 9223372036854775805,
    "status": "enabled",
    "using": [
      "traefik"
    ],
    "name": "dashboard@internal",
    "provider": "internal"
  },
  {
    "entryPoints": [
      "web",
      "web2"
    ],
    "service": "whoami-traefiktest",
    "rule": "Path(`/`)",
    "priority": 9,
    "status": "enabled",
    "using": [
      "web",
      "web2"
    ],
    "name": "whoami@docker",
    "provider": "docker"
  }
]
```
</details>
<details>
<summary>Using `traefik:v3.3.2` docker image</summary>

```
% curl -s "http://<IP>:8090/api/http/routers" | jq .
[
  {
    "entryPoints": [
      "traefik"
    ],
    "service": "api@internal",
    "rule": "PathPrefix(`/api`)",
    "ruleSyntax": "v3",
    "priority": 9223372036854775806,
    "observability": {
      "accessLogs": true,
      "tracing": true,
      "metrics": true
    },
    "status": "enabled",
    "using": [
      "traefik"
    ],
    "name": "api@internal",
    "provider": "internal"
  },
  {
    "entryPoints": [
      "web"
    ],
    "service": "api@internal",
    "rule": "(PathPrefix(`/dashboard`) || PathPrefix(`/api`))",
    "priority": 48,
    "observability": {
      "accessLogs": true,
      "tracing": true,
      "metrics": true
    },
    "status": "enabled",
    "using": [
      "web"
    ],
    "name": "dashboard@docker",
    "provider": "docker"
  },
  {
    "entryPoints": [
      "traefik"
    ],
    "middlewares": [
      "dashboard_redirect@internal",
      "dashboard_stripprefix@internal"
    ],
    "service": "dashboard@internal",
    "rule": "PathPrefix(`/`)",
    "ruleSyntax": "v3",
    "priority": 9223372036854775805,
    "observability": {
      "accessLogs": true,
      "tracing": true,
      "metrics": true
    },
    "status": "enabled",
    "using": [
      "traefik"
    ],
    "name": "dashboard@internal",
    "provider": "internal"
  },
  {
    "entryPoints": [
      "web"
    ],
    "service": "whoami-traefiktest",
    "rule": "Path(`/`)",
    "priority": 9,
    "observability": {
      "accessLogs": true,
      "tracing": true,
      "metrics": true
    },
    "status": "enabled",
    "using": [
      "web"
    ],
    "name": "web-whoami@docker",
    "provider": "docker"
  },
  {
    "entryPoints": [
      "web2"
    ],
    "service": "whoami-traefiktest",
    "rule": "Path(`/`)",
    "priority": 9,
    "observability": {
      "accessLogs": true,
      "tracing": true,
      "metrics": true
    },
    "status": "enabled",
    "using": [
      "web2"
    ],
    "name": "web2-whoami@docker",
    "provider": "docker"
  }
]
```
</details>

<details>

<summary>Diff between v3.3.1 and v3.3.2</summary>

```
% diff -uw v3.3.1.json v3.3.2.json
--- v3.3.1.json 2025-01-17 15:41:47.643409277 +0100
+++ v3.3.2.json 2025-01-17 15:41:15.105948017 +0100
@@ -7,6 +7,11 @@
     "rule": "PathPrefix(`/api`)",
     "ruleSyntax": "v3",
     "priority": 9223372036854775806,
+    "observability": {
+      "accessLogs": true,
+      "tracing": true,
+      "metrics": true
+    },
     "status": "enabled",
     "using": [
       "traefik"
@@ -21,6 +26,11 @@
     "service": "api@internal",
     "rule": "(PathPrefix(`/dashboard`) || PathPrefix(`/api`))",
     "priority": 48,
+    "observability": {
+      "accessLogs": true,
+      "tracing": true,
+      "metrics": true
+    },
     "status": "enabled",
     "using": [
       "web"
@@ -40,6 +50,11 @@
     "rule": "PathPrefix(`/`)",
     "ruleSyntax": "v3",
     "priority": 9223372036854775805,
+    "observability": {
+      "accessLogs": true,
+      "tracing": true,
+      "metrics": true
+    },
     "status": "enabled",
     "using": [
       "traefik"
@@ -49,18 +64,40 @@
   },
   {
     "entryPoints": [
-      "web",
+      "web"
+    ],
+    "service": "whoami-traefiktest",
+    "rule": "Path(`/`)",
+    "priority": 9,
+    "observability": {
+      "accessLogs": true,
+      "tracing": true,
+      "metrics": true
+    },
+    "status": "enabled",
+    "using": [
+      "web"
+    ],
+    "name": "web-whoami@docker",
+    "provider": "docker"
+  },
+  {
+    "entryPoints": [
       "web2"
     ],
     "service": "whoami-traefiktest",
     "rule": "Path(`/`)",
     "priority": 9,
+    "observability": {
+      "accessLogs": true,
+      "tracing": true,
+      "metrics": true
+    },
     "status": "enabled",
     "using": [
-      "web",
       "web2"
     ],
-    "name": "whoami@docker",
+    "name": "web2-whoami@docker",
     "provider": "docker"
   }
 ]
```

</details>
Hello @frsk,

Thanks for reporting this and sharing the reproducible case!

I have been able to confirm the repetition of entries, and I have found some explanations.

We recently introduced the ability to manage observability per router/entryPoint (https://github.com/traefik/traefik/pull/11308) and also brought a fix for it in v3.3.2 (https://github.com/traefik/traefik/pull/11446).

This feature leverages the entryPoints "models", which allow to configure options on entryPoints that would be applied to any router attached to those entryPoints.
In the case of the observability per entryPoint, this means having a new observability configuration node on both entryPoints and routers, for which, by default, each option to enable a kind of signal, is true.

However, as a consequence, this enablement by default now always creates a model for each entryPoint, where, before, without [specific options](https://doc.traefik.io/traefik/routing/entrypoints/#http-options), it was never the case.

When applying the models, this removes the possibility of having no models and produce a single version of a router attached to multiple entryPoint:
https://github.com/traefik/traefik/blob/95dd17e0200e5a861e4519675d2f37acfc7b95eb/pkg/server/aggregator.go#L173-L212
Hi @rtribotte I want to contribute to this issue. Basically, now we are flattening the routers - endpoints map into multiple router-endpoint pairs in every case since there will always be internal models attached after this [fix](https://github.com/traefik/traefik/pull/11446/files#diff-090a993c4d91211f295035e242da8f9572e3749de11d0113e70f5b34c47db725) . 

I have the following solutions in mind:
1. The ideal solution would be to store the endpoint struct inside the endpoints array in the router config and flatten only when needed by the middlewares but it will require changes in all the downstream files consuming router.
2. Create a separate API view for the routers that reconstructs the original router map config before applyModel.
3. Let the new behaviour be the default behaviour

Please share your view on this

Fix patch:
diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go
index f443abe378..59c75bfe40 100644
--- a/pkg/config/static/entrypoints.go
+++ b/pkg/config/static/entrypoints.go
@@ -60,8 +60,6 @@ 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.
@@ -164,14 +162,15 @@ func (u *UDPConfig) SetDefaults() {
 
 // ObservabilityConfig holds the observability configuration for an entry point.
 type ObservabilityConfig struct {
-	AccessLogs bool `json:"accessLogs,omitempty" toml:"accessLogs,omitempty" yaml:"accessLogs,omitempty" export:"true"`
-	Tracing    bool `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" export:"true"`
-	Metrics    bool `json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
+	AccessLogs *bool `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
+	defaultValue := true
+	o.AccessLogs = &defaultValue
+	o.Tracing = &defaultValue
+	o.Metrics = &defaultValue
 }
diff --git a/pkg/provider/traefik/internal.go b/pkg/provider/traefik/internal.go
index 47f8223213..f1490a41bd 100644
--- a/pkg/provider/traefik/internal.go
+++ b/pkg/provider/traefik/internal.go
@@ -240,9 +240,9 @@ func (i *Provider) entryPointModels(cfg *dynamic.Configuration) {
 
 		if ep.Observability != nil {
 			httpModel.Observability = dynamic.RouterObservabilityConfig{
-				AccessLogs: &ep.Observability.AccessLogs,
-				Tracing:    &ep.Observability.Tracing,
-				Metrics:    &ep.Observability.Metrics,
+				AccessLogs: ep.Observability.AccessLogs,
+				Tracing:    ep.Observability.Tracing,
+				Metrics:    ep.Observability.Metrics,
 			}
 		}
 
diff --git a/pkg/server/aggregator.go b/pkg/server/aggregator.go
index 72934d4690..3cd97f2066 100644
--- a/pkg/server/aggregator.go
+++ b/pkg/server/aggregator.go
@@ -191,14 +191,14 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
 						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
 					}
 
+					if cp.Observability.Tracing == nil {
+						cp.Observability.Tracing = m.Observability.Tracing
+					}
+
 					rtName := name
 					if len(eps) > 1 {
 						rtName = epName + "-" + name
@@ -215,6 +215,9 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
 		cfg.HTTP.Routers = rts
 	}
 
+	// Apply default observability model to HTTP routers.
+	applyDefaultObservabilityModel(cfg)
+
 	if cfg.TCP == nil || len(cfg.TCP.Models) == 0 {
 		return cfg
 	}
@@ -238,3 +241,38 @@ func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
 
 	return cfg
 }
+
+// applyDefaultObservabilityModel applies the default observability model to the configuration.
+// This function is used to ensure that the observability configuration is set for all routers,
+// and make sure it is serialized and available in the API.
+// We could have introduced a "default" model, but it would have been more complex to manage for now.
+// This could be generalized in the future.
+func applyDefaultObservabilityModel(cfg dynamic.Configuration) {
+	if cfg.HTTP != nil {
+		for _, router := range cfg.HTTP.Routers {
+			if router.Observability == nil {
+				router.Observability = &dynamic.RouterObservabilityConfig{
+					AccessLogs: pointer(true),
+					Metrics:    pointer(true),
+					Tracing:    pointer(true),
+				}
+
+				continue
+			}
+
+			if router.Observability.AccessLogs == nil {
+				router.Observability.AccessLogs = pointer(true)
+			}
+
+			if router.Observability.Tracing == nil {
+				router.Observability.Tracing = pointer(true)
+			}
+
+			if router.Observability.Metrics == nil {
+				router.Observability.Metrics = pointer(true)
+			}
+		}
+	}
+}
+
+func pointer[T any](v T) *T { return &v }
diff --git a/pkg/server/middleware/observability.go b/pkg/server/middleware/observability.go
index 566d8522ca..d279be9028 100644
--- a/pkg/server/middleware/observability.go
+++ b/pkg/server/middleware/observability.go
@@ -110,7 +110,7 @@ func (o *ObservabilityMgr) ShouldAddAccessLogs(serviceName string, observability
 		return false
 	}
 
-	return observabilityConfig == nil || observabilityConfig.AccessLogs != nil && *observabilityConfig.AccessLogs
+	return observabilityConfig == nil || observabilityConfig.AccessLogs == nil || *observabilityConfig.AccessLogs
 }
 
 // ShouldAddMetrics returns whether the metrics should be enabled for the given resource and the observability config.
@@ -127,7 +127,7 @@ func (o *ObservabilityMgr) ShouldAddMetrics(serviceName string, observabilityCon
 		return false
 	}
 
-	return observabilityConfig == nil || observabilityConfig.Metrics != nil && *observabilityConfig.Metrics
+	return observabilityConfig == nil || observabilityConfig.Metrics == nil || *observabilityConfig.Metrics
 }
 
 // ShouldAddTracing returns whether the tracing should be enabled for the given serviceName and the observability config.
@@ -144,7 +144,7 @@ func (o *ObservabilityMgr) ShouldAddTracing(serviceName string, observabilityCon
 		return false
 	}
 
-	return observabilityConfig == nil || observabilityConfig.Tracing != nil && *observabilityConfig.Tracing
+	return observabilityConfig == nil || observabilityConfig.Tracing == nil || *observabilityConfig.Tracing
 }
 
 // MetricsRegistry is an accessor to the metrics registry.


Test patch:
diff --git a/pkg/config/static/static_config_test.go b/pkg/config/static/static_config_test.go
index 78633bc2f1..67c643a32a 100644
--- a/pkg/config/static/static_config_test.go
+++ b/pkg/config/static/static_config_test.go
@@ -77,11 +77,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
 					UDP: &UDPConfig{
 						Timeout: 3000000000,
 					},
-					Observability: &ObservabilityConfig{
-						AccessLogs: true,
-						Tracing:    true,
-						Metrics:    true,
-					},
 				}},
 				Providers: &Providers{},
 			},
@@ -127,11 +122,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
 					UDP: &UDPConfig{
 						Timeout: 3000000000,
 					},
-					Observability: &ObservabilityConfig{
-						AccessLogs: true,
-						Tracing:    true,
-						Metrics:    true,
-					},
 				}},
 				Providers: &Providers{},
 				CertificatesResolvers: map[string]CertificateResolver{
@@ -188,11 +178,6 @@ func TestConfiguration_SetEffectiveConfiguration(t *testing.T) {
 					UDP: &UDPConfig{
 						Timeout: 3000000000,
 					},
-					Observability: &ObservabilityConfig{
-						AccessLogs: true,
-						Tracing:    true,
-						Metrics:    true,
-					},
 				}},
 				Providers: &Providers{},
 				CertificatesResolvers: map[string]CertificateResolver{
@@ -253,11 +238,6 @@ 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/provider/traefik/internal_test.go b/pkg/provider/traefik/internal_test.go
index ed7197f827..3ff89a727e 100644
--- a/pkg/provider/traefik/internal_test.go
+++ b/pkg/provider/traefik/internal_test.go
@@ -18,6 +18,8 @@ import (
 
 var updateExpected = flag.Bool("update_expected", false, "Update expected files in fixtures")
 
+func pointer[T any](v T) *T { return &v }
+
 func Test_createConfiguration(t *testing.T) {
 	testCases := []struct {
 		desc      string
@@ -185,9 +187,9 @@ func Test_createConfiguration(t *testing.T) {
 							},
 						},
 						Observability: &static.ObservabilityConfig{
-							AccessLogs: false,
-							Tracing:    false,
-							Metrics:    false,
+							AccessLogs: pointer(false),
+							Tracing:    pointer(false),
+							Metrics:    pointer(false),
 						},
 					},
 				},
diff --git a/pkg/server/aggregator_test.go b/pkg/server/aggregator_test.go
index 50a400d2ac..182815a395 100644
--- a/pkg/server/aggregator_test.go
+++ b/pkg/server/aggregator_test.go
@@ -9,8 +9,6 @@ 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
@@ -508,6 +506,33 @@ func Test_applyModel(t *testing.T) {
 				},
 			},
 		},
+		{
+			desc: "without model, one router",
+			input: dynamic.Configuration{
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers:     map[string]*dynamic.Router{"test": {}},
+					Middlewares: make(map[string]*dynamic.Middleware),
+					Services:    make(map[string]*dynamic.Service),
+					Models:      make(map[string]*dynamic.Model),
+				},
+			},
+			expected: dynamic.Configuration{
+				HTTP: &dynamic.HTTPConfiguration{
+					Routers: map[string]*dynamic.Router{
+						"test": {
+							Observability: &dynamic.RouterObservabilityConfig{
+								AccessLogs: pointer(true),
+								Metrics:    pointer(true),
+								Tracing:    pointer(true),
+							},
+						},
+					},
+					Middlewares: make(map[string]*dynamic.Middleware),
+					Services:    make(map[string]*dynamic.Service),
+					Models:      make(map[string]*dynamic.Model),
+				},
+			},
+		},
 		{
 			desc: "with model, not used",
 			input: dynamic.Configuration{
@@ -560,10 +585,14 @@ func Test_applyModel(t *testing.T) {
 				HTTP: &dynamic.HTTPConfiguration{
 					Routers: map[string]*dynamic.Router{
 						"test": {
-							EntryPoints:   []string{"websecure"},
-							Middlewares:   []string{"test"},
-							TLS:           &dynamic.RouterTLSConfig{},
-							Observability: &dynamic.RouterObservabilityConfig{},
+							EntryPoints: []string{"websecure"},
+							Middlewares: []string{"test"},
+							TLS:         &dynamic.RouterTLSConfig{},
+							Observability: &dynamic.RouterObservabilityConfig{
+								AccessLogs: pointer(true),
+								Metrics:    pointer(true),
+								Tracing:    pointer(true),
+							},
 						},
 					},
 					Middlewares: make(map[string]*dynamic.Middleware),
@@ -659,9 +688,9 @@ func Test_applyModel(t *testing.T) {
 							Middlewares: []string{"test"},
 							TLS:         &dynamic.RouterTLSConfig{CertResolver: "router"},
 							Observability: &dynamic.RouterObservabilityConfig{
-								AccessLogs: nil,
-								Tracing:    nil,
-								Metrics:    nil,
+								AccessLogs: pointer(true),
+								Metrics:    pointer(true),
+								Tracing:    pointer(true),
 							},
 						},
 					},
@@ -700,12 +729,21 @@ func Test_applyModel(t *testing.T) {
 					Routers: map[string]*dynamic.Router{
 						"test": {
 							EntryPoints: []string{"web"},
+							Observability: &dynamic.RouterObservabilityConfig{
+								AccessLogs: pointer(true),
+								Metrics:    pointer(true),
+								Tracing:    pointer(true),
+							},
 						},
 						"websecure-test": {
-							EntryPoints:   []string{"websecure"},
-							Middlewares:   []string{"test"},
-							TLS:           &dynamic.RouterTLSConfig{},
-							Observability: &dynamic.RouterObservabilityConfig{},
+							EntryPoints: []string{"websecure"},
+							Middlewares: []string{"test"},
+							TLS:         &dynamic.RouterTLSConfig{},
+							Observability: &dynamic.RouterObservabilityConfig{
+								AccessLogs: pointer(true),
+								Metrics:    pointer(true),
+								Tracing:    pointer(true),
+							},
 						},
 					},
 					Middlewares: make(map[string]*dynamic.Middleware),
diff --git a/pkg/server/configurationwatcher_test.go b/pkg/server/configurationwatcher_test.go
index 7c4ad735f0..7baf233d05 100644
--- a/pkg/server/configurationwatcher_test.go
+++ b/pkg/server/configurationwatcher_test.go
@@ -84,7 +84,8 @@ func TestNewConfigurationWatcher(t *testing.T) {
 				th.WithRouters(
 					th.WithRouter("test@mock",
 						th.WithEntryPoints("e"),
-						th.WithServiceName("scv"))),
+						th.WithServiceName("scv"),
+						th.WithObservability())),
 				th.WithMiddlewares(),
 				th.WithLoadBalancerServices(),
 			),
@@ -175,7 +176,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
 
 	expectedConfig := dynamic.Configuration{
 		HTTP: th.BuildConfiguration(
-			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
+			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
 			th.WithLoadBalancerServices(th.WithService("bar@mock")),
 			th.WithMiddlewares(),
 		),
@@ -200,7 +201,7 @@ func TestIgnoreTransientConfiguration(t *testing.T) {
 
 	expectedConfig3 := dynamic.Configuration{
 		HTTP: th.BuildConfiguration(
-			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
+			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
 			th.WithLoadBalancerServices(th.WithService("bar-config3@mock")),
 			th.WithMiddlewares(),
 		),
@@ -447,7 +448,7 @@ func TestListenProvidersDoesNotSkipFlappingConfiguration(t *testing.T) {
 
 	expected := dynamic.Configuration{
 		HTTP: th.BuildConfiguration(
-			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
+			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
 			th.WithLoadBalancerServices(th.WithService("bar@mock")),
 			th.WithMiddlewares(),
 		),
@@ -538,7 +539,7 @@ func TestListenProvidersIgnoreSameConfig(t *testing.T) {
 
 	expected := dynamic.Configuration{
 		HTTP: th.BuildConfiguration(
-			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"))),
+			th.WithRouters(th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability())),
 			th.WithLoadBalancerServices(th.WithService("bar@mock")),
 			th.WithMiddlewares(),
 		),
@@ -674,7 +675,7 @@ func TestListenProvidersIgnoreIntermediateConfigs(t *testing.T) {
 
 	expected := dynamic.Configuration{
 		HTTP: th.BuildConfiguration(
-			th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"))),
+			th.WithRouters(th.WithRouter("final@mock", th.WithEntryPoints("ep"), th.WithObservability())),
 			th.WithLoadBalancerServices(th.WithService("final@mock")),
 			th.WithMiddlewares(),
 		),
@@ -738,8 +739,8 @@ func TestListenProvidersPublishesConfigForEachProvider(t *testing.T) {
 	expected := dynamic.Configuration{
 		HTTP: th.BuildConfiguration(
 			th.WithRouters(
-				th.WithRouter("foo@mock", th.WithEntryPoints("ep")),
-				th.WithRouter("foo@mock2", th.WithEntryPoints("ep")),
+				th.WithRouter("foo@mock", th.WithEntryPoints("ep"), th.WithObservability()),
+				th.WithRouter("foo@mock2", th.WithEntryPoints("ep"), th.WithObservability()),
 			),
 			th.WithLoadBalancerServices(
 				th.WithService("bar@mock"),
diff --git a/pkg/testhelpers/config.go b/pkg/testhelpers/config.go
index 3f379f6bd0..2bbea19564 100644
--- a/pkg/testhelpers/config.go
+++ b/pkg/testhelpers/config.go
@@ -53,6 +53,17 @@ func WithServiceName(serviceName string) func(*dynamic.Router) {
 	}
 }
 
+// WithObservability is a helper to create a configuration.
+func WithObservability() func(*dynamic.Router) {
+	return func(r *dynamic.Router) {
+		r.Observability = &dynamic.RouterObservabilityConfig{
+			AccessLogs: pointer(true),
+			Metrics:    pointer(true),
+			Tracing:    pointer(true),
+		}
+	}
+}
+
 // WithLoadBalancerServices is a helper to create a configuration.
 func WithLoadBalancerServices(opts ...func(service *dynamic.ServersLoadBalancer) string) func(*dynamic.HTTPConfiguration) {
 	return func(c *dynamic.HTTPConfiguration) {
@@ -149,3 +160,5 @@ func WithSticky(cookieName string) func(*dynamic.ServersLoadBalancer) {
 		}
 	}
 }
+
+func pointer[T any](v T) *T { return &v }