Conversation
…n Jaeger The `http.middleware.otel` module was instantiated but its `Process()` method was never called — it had no path into the HTTP handler chain. Two changes fix this: 1. `StandardHTTPRouter.AddGlobalMiddleware()` — new method that appends a middleware to a per-router slice. `ServeHTTP` now wraps the inner mux with all registered global middlewares before dispatching, so every request (health, metrics, pipeline triggers, and static routes) is observed. 2. `observability.otel-middleware` wiring hook (priority 100) — runs at startup and calls `AddGlobalMiddleware` for every `OTelMiddleware` found in the service registry, connecting it to every `StandardHTTPRouter`. The observability plugin manifest and tests are updated to reflect the new wiring hook (5 total) and the already-present `http.middleware.otel` module type (6 total). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR wires the OpenTelemetry HTTP middleware into the request handling path by adding support for router-level “global” middleware and registering the OTEL middleware on all StandardHTTPRouter instances via an observability wiring hook, so inbound requests produce trace spans (e.g., visible in Jaeger).
Changes:
- Add
StandardHTTPRouter.AddGlobalMiddleware()and applyglobalMiddlewaresaround the entire mux inServeHTTP. - Add a new observability wiring hook (
observability.otel-middleware, priority 100) to registerOTelMiddlewareglobally on all routers. - Update observability plugin manifest/tests to include the new wiring hook and module type counts.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
module/http_router.go |
Adds global middleware support and wraps the router’s mux with a global middleware chain in ServeHTTP. |
plugins/observability/wiring.go |
Introduces wireOTelMiddleware wiring hook to attach OTelMiddleware to all StandardHTTPRouters. |
plugins/observability/plugin.go |
Registers the new wiring hook name in the observability plugin manifest. |
plugins/observability/plugin_test.go |
Updates manifest/module type/wiring hook count expectations and hook name list. |
| func (r *StandardHTTPRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) { | ||
| r.mu.RLock() | ||
| defer r.mu.RUnlock() | ||
|
|
There was a problem hiding this comment.
ServeHTTP holds r.mu.RLock for the entire request (due to the deferred RUnlock) and then calls middleware Process() and handler.ServeHTTP while the lock is held. This risks self-deadlock if any handler/middleware tries to mutate the router (e.g., AddRouteWithMiddleware/AddGlobalMiddleware) and also blocks hot-add route updates until the request completes. Consider copying serveMux and globalMiddlewares under the lock, releasing the lock, then building the middleware chain and serving the request without holding r.mu.
| defer r.mu.Unlock() | ||
| r.globalMiddlewares = append(r.globalMiddlewares, mw) |
There was a problem hiding this comment.
AddGlobalMiddleware/ServeHTTP introduce new global middleware behavior, but there are existing router tests (module/http_test.go) that don't currently validate that global middlewares run (and run in the expected order) for matched routes and 404s. Adding unit tests for AddGlobalMiddleware + ServeHTTP would help prevent regressions in tracing and other cross-cutting middleware.
| defer r.mu.Unlock() | |
| r.globalMiddlewares = append(r.globalMiddlewares, mw) | |
| defer r.mu.Unlock() | |
| r.globalMiddlewares = append(r.globalMiddlewares, mw) | |
| // Rebuild the mux if we've already started so that newly added global | |
| // middlewares take effect for all subsequent requests (hot-add support). | |
| if r.serveMux != nil { | |
| r.rebuildMuxLocked() | |
| } |
| for _, svc := range app.SvcRegistry() { | ||
| otelMw, ok := svc.(*module.OTelMiddleware) | ||
| if !ok { | ||
| continue | ||
| } | ||
| for _, routerSvc := range app.SvcRegistry() { | ||
| router, ok := routerSvc.(*module.StandardHTTPRouter) | ||
| if !ok { | ||
| continue | ||
| } | ||
| router.AddGlobalMiddleware(otelMw) | ||
| } | ||
| } |
There was a problem hiding this comment.
wireOTelMiddleware currently does a nested iteration over app.SvcRegistry() (OTelMiddleware × routers). Even if the registry is usually small, this is avoidable and makes the hook harder to extend. Consider collecting the routers once (and possibly the OTel middlewares once) and then wiring them in a single pass.
| for _, svc := range app.SvcRegistry() { | |
| otelMw, ok := svc.(*module.OTelMiddleware) | |
| if !ok { | |
| continue | |
| } | |
| for _, routerSvc := range app.SvcRegistry() { | |
| router, ok := routerSvc.(*module.StandardHTTPRouter) | |
| if !ok { | |
| continue | |
| } | |
| router.AddGlobalMiddleware(otelMw) | |
| } | |
| } | |
| var ( | |
| routers []*module.StandardHTTPRouter | |
| middlewares []*module.OTelMiddleware | |
| ) | |
| // Collect all routers and OTel middleware instances in a single pass. | |
| for _, svc := range app.SvcRegistry() { | |
| if router, ok := svc.(*module.StandardHTTPRouter); ok { | |
| routers = append(routers, router) | |
| continue | |
| } | |
| if otelMw, ok := svc.(*module.OTelMiddleware); ok { | |
| middlewares = append(middlewares, otelMw) | |
| } | |
| } | |
| // Register each OTel middleware as global middleware on every router. | |
| for _, router := range routers { | |
| for _, otelMw := range middlewares { | |
| router.AddGlobalMiddleware(otelMw) | |
| } | |
| } |
Summary
OTelMiddleware.Process()was never called — the module was instantiated but had no path into the HTTP handler chain, so no traces were emitted.StandardHTTPRouter.AddGlobalMiddleware()which wraps the entire mux before dispatching, ensuring every request (health probes, metrics, pipeline triggers, static files) gets a trace span.observability.otel-middlewarewiring hook (priority 100) that runs at startup and registers anyOTelMiddlewarefound in the service registry as a global middleware on everyStandardHTTPRouter.Root cause
StandardHTTPRouter.ServeHTTPdelegated directly to the innerserveMuxwith no outer middleware chain. Per-route middlewares (viaAddRouteWithMiddleware) were available, but the OTEL middleware module had no wiring hook to attach itself — it just registered as a service and sat unused.Changes
module/http_router.goglobalMiddlewares []HTTPMiddlewarefield;AddGlobalMiddleware()method;ServeHTTPapplies global chain before muxplugins/observability/wiring.gowireOTelMiddlewarehook at priority 100plugins/observability/plugin.goobservability.otel-middlewarein manifestWiringHooksplugins/observability/plugin_test.goTest plan
go test ./...— all packages passcurl http://localhost:8080/...workflow-cloudappears incurl -s http://localhost:16686/api/services🤖 Generated with Claude Code