Skip to content

feat: add routing hook and scope["route"] for observability instrumentation#2110

Open
thanhlim wants to merge 3 commits intospec-first:mainfrom
ThriveMarket:refactor/rename-routing-hook
Open

feat: add routing hook and scope["route"] for observability instrumentation#2110
thanhlim wants to merge 3 commits intospec-first:mainfrom
ThriveMarket:refactor/rename-routing-hook

Conversation

@thanhlim
Copy link

@thanhlim thanhlim commented Mar 4, 2026

Summary

This PR adds two related features to improve observability instrumentation compatibility.

Relates to #2036

1. Set scope["route"] for OpenTelemetry/ASGI instrumentation

OpenTelemetry's ASGI middleware reads scope["route"].path to populate the http.route span attribute. Without this, APM tools like NewRelic show transaction names with actual path values (e.g., /users/12345) instead of templated routes (e.g., /users/{user_id}), making it impossible to aggregate metrics properly.

This change sets scope["route"] to a SimpleNamespace(path=full_route_path) after routing resolution, where full_route_path includes the API base path and the OpenAPI path template.

2. Add after_routing_resolution hook mechanism

Provides a general-purpose hook that runs after Connexion determines which operation handles a request, but before the operation is invoked. This enables:

  • Observability: Set span attributes, update trace names (OTEL, Datadog, etc.)
  • Logging: Log route information with structured context
  • Metrics: Record routing metrics or counters
  • Auditing: Track which operations are called

Usage

from connexion.middleware.routing import RoutingOperation

def my_hook(route_path: str, operation_id: str | None, scope: dict) -> None:
    # route_path: "/v1/users/{user_id}" (OpenAPI template)
    # operation_id: "get_user" (from OpenAPI spec)
    # scope: ASGI scope dict
    print(f"Routing {scope['method']} to {route_path}")

RoutingOperation.after_routing_resolution(my_hook)

Hook errors are caught and logged at DEBUG level to avoid breaking request processing.

Test Plan

  • Added RoutePathMiddleware test to verify scope["route"].path is set correctly
  • Added TestRoutingHooks class with tests for:
    • Hook receives correct route_path, operation_id, and scope
    • Multiple hooks are all invoked
    • Hook errors don't break request processing

thanhlim and others added 2 commits March 4, 2026 13:03
* feat: set scope["route"] for OpenTelemetry instrumentation

OpenTelemetry's ASGI middleware reads scope["route"].path to populate
the http.route attribute on spans and metrics. Without this, APM tools
like NewRelic show incomplete transaction names (e.g., "POST" instead
of "POST /v2/notifications/{ticket_id}").

This change stores the OpenAPI path template in RoutingOperation and
sets scope["route"] after routing resolves, enabling automatic
http.route population without per-service workarounds.

Fixes spec-first#2036

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add tests for scope['route'] OTEL compatibility

- Add RoutePathMiddleware test helper that reads scope['route'].path
- Add test_route_path_for_otel to verify route path is set correctly
- Add test_route_path_with_multiple_params for paths with multiple params

These tests verify that the routing middleware correctly sets
scope['route'].path for OpenTelemetry ASGI instrumentation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add on_route_resolved callback hook for observability integration

- Add RouteResolvedCallback type for callback signature
- Add on_route_resolved() class method to register callbacks
- Add clear_route_callbacks() for testing
- Callbacks receive (route_path, operation_id, scope)
- Callback errors are silently caught to not break requests

This enables services to integrate with OpenTelemetry or other
observability tools without adding dependencies to Connexion itself.

Example usage:
    from connexion.middleware.routing import RoutingOperation
    from opentelemetry import trace

    def update_otel_span(route_path, operation_id, scope):
        span = trace.get_current_span()
        if span.is_recording():
            span.set_attribute("http.route", route_path)

    RoutingOperation.on_route_resolved(update_otel_span)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: add logging for callback errors, fix import order

* fix: configure bandit to skip B101 in test files

* fix: add nosec B101 comments to test asserts

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Make the routing hook more general-purpose:
- on_route_resolved() -> after_routing_resolution()
- clear_route_callbacks() -> clear_routing_hooks()
- RouteResolvedCallback -> RoutingHook
- _route_callbacks -> _routing_hooks

Updated docstring to describe broader use cases:
- Observability (OTEL, Datadog, etc.)
- Logging
- Metrics
- Auditing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@thanhlim thanhlim changed the title refactor: rename routing hook to after_routing_resolution feat: add routing hook and scope["route"] for observability instrumentation Mar 4, 2026
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant