ENG-9166 feat(otel): built-in OpenTelemetry trace points, metrics and the reflex-otel instrumentor (1/3)

Merged
FA
1mo ago
+2034-9

Part of #6227 (ENG-9166). Stack: 1/3 (this) โ†’ 2/3 compile spans + log correlation โ†’ 3/3 browser plugin.

What

Makes the framework instrumentation-aware but zero-cost without the instrumentation package:

  • reflex-base gets a hard dependency on opentelemetry-api (>=1.30). All trace points live in reflex_base/otel.py behind a module-level enabled bool โ€” one attribute read per event when off, no OTel objects created.
  • New package packages/reflex-otel (reflex_otel.ReflexInstrumentor, a BaseInstrumentor with the standard opentelemetry_instrumentor entry point) flips the flag.

When enabled:

  • One span per event handler run: SERVER for events from the frontend (new trace, or child of the browser span if the event carries a traceparent field), INTERNAL for chained events, parented under the span that enqueued them via a context snapshot in EventContext.fork(). Attributes: reflex.event.name/txid/parent_txid/background, session.id, code.function.name; exceptions recorded on the span. Events are never parented under the websocket span.
  • The ASGI app is wrapped in the contrib ASGI middleware (/ping excluded, per-message websocket spans off).
  • Metrics: reflex.event.duration (+error.type), reflex.state.acquire.duration, reflex.websocket.message.size, reflex.websocket.connections.

Design

Follows the pattern used by Shiny for Python and pydantic-ai (first-party trace points + opentelemetry-api no-op) rather than wrapt monkeypatching. Survey of other frameworks and the measured no-op costs are in the ticket.

Notes

  • reflex-otel pins reflex-base >= <workspace dev version> like the other workspace packages; re-pin at the next release (check_min_deps --check-dev-pins is the publish gate).
  • Registered in publish.yml, dispatch_release.yml, detect.sh.
  • Verified end to end with Jaeger (traces) and Prometheus (metrics) on a demo app.

Tests

tests/units/reflex_base/test_otel.py, tests/units/reflex_otel/, otel cases in test_app.py, test_event_processor.py, test_base_state_processor.py; shared otel_sdk fixture in tests/units/conftest.py.