Skip to content

Use FastAPI's native OpenTelemetry for HTTP instrumentation - #249

Merged
Subhransu-De merged 2 commits into
mainfrom
feat/fastapi-native-otel
Oct 11, 2026
Merged

Subhransu-De merged 2 commits into
mainfrom
feat/fastapi-native-otel

Conversation

@Subhransu-De

Copy link
Copy Markdown
Owner

Summary

FastAPI 0.142.0 added native OpenTelemetry support (fastapi#16403). This repo already passed telemetry=native_telemetry_config(), but the native layer never ran. logfire.instrument_fastapi adds the contrib OpenTelemetryMiddleware, and FastAPI keeps its own telemetry dormant whenever that middleware is in the stack.

This PR removes the Logfire FastAPI instrumentation so the native integration takes over. Logfire still owns the providers and exporters (logfire.configure) and the SQLAlchemy instrumentation.

What changes in the telemetry

Before (Logfire / contrib ASGI) After (FastAPI native)
Request span GET /entities/{entity_id} with http.method, http.status_code Same name, with http.request.method, http.response.status_code, url.path, server.*
Operation spans none fastapi.dependencies, fastapi.endpoint, fastapi.serialization, fastapi.background_task
Metrics none http.server.request.duration, http.server.active_requests
Logs none http.server.request.exception (with stack trace), fastapi.validation.failed (error count and route)
Client IP client.ip via request_attributes_mapper client.address, set by ServerSpanMiddleware
Validation errors on span errors (pydantic errors, which include input values) not on the span; the validation log carries the count
Health probes excluded by URL regex excluded by exclude on the ASGI path

Code

  • app/telemetry.py
    • Removes instrument_fastapi, the request-attributes mapper and the URL regex.
    • Adds ServerSpanMiddleware, a pure ASGI middleware. FastAPI runs dependencies inside a fastapi.dependencies child span, so the middleware stores the request span in the OpenTelemetry context. record_auth_attributes then tags the request span with oidc.*, not the child span. It also sets client.address.
    • native_telemetry_config() is unchanged: auto_configure stays off so platform OTEL_* variables cannot add a second exporter.
  • app/main.py: registers the middleware in place of instrument_fastapi.
  • pyproject.toml: fastapi>=0.143.0. 0.142.4 isolates telemetry for excluded requests, and 0.143.0 makes exporter auto-configuration opt-in.
  • AGENTS.md and README.md describe the new ownership split.

Tests

  • Unit tests drive a real FastAPI app through the native telemetry with capfire. They check the request span (route, status, client.address, oidc.*), that auth attributes are not on the dependencies span, and that health probes produce no spans.
  • tests/integration/test_otel_api.py now uses the new attribute names. It still checks that no request body or bearer token reaches any exported attribute.
  • make check passes, and integration tests pass on Postgres (74 passed).

Opened by claude-opus-5-5

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-11T06:22:29.792603Z bd62cf1 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

FastAPI 0.142 added native OpenTelemetry. This repo already passed a
telemetry config, but logfire.instrument_fastapi added the contrib ASGI
middleware, so FastAPI kept its own telemetry dormant. Drop the Logfire
FastAPI instrumentation and let the native integration run on the
providers that logfire.configure installs.

- Request spans now use current semantic conventions and get fastapi.*
  child spans for dependencies, endpoint and serialization, plus
  http.server.request.duration and active-request metrics, and log
  events for unhandled exceptions and validation failures.
- ServerSpanMiddleware sets client.address on the request span and keeps
  that span reachable, so record_auth_attributes tags the request rather
  than the fastapi.dependencies span.
- Require fastapi>=0.143.0: 0.142.4 isolates excluded requests, and
  0.143.0 makes exporter auto-configuration opt-in.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0f46daa8e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/telemetry.py Outdated
+ "|".join(re.escape(path) for path in HEALTH_ENDPOINT_PATHS)
+ r")(?:\?.*)?$"
)
_SERVER_SPAN_KEY = context.create_key("app.server_span")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the span key out of module scope

Creating _SERVER_SPAN_KEY here generates and retains a process-global identity as soon as app.telemetry is imported, putting telemetry state outside the application/lifespan lifecycle and directly violating the repository's prohibition on module-level singletons. Move ownership of this key into an application-scoped component rather than initializing it during import.

AGENTS.md reference: AGENTS.md:L69-L69

Useful? React with 👍 / 👎.

@Subhransu-De Subhransu-De Oct 11, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly applied in bd62cf1. The key is now the string literal "app.telemetry.server_span", so nothing runs at import time.

I kept it at module scope. It is an immutable constant name, not mutable state or a resource, so the AGENTS.md rule against module-level singletons does not apply, in the same way it does not apply to HEALTH_ENDPOINT_PATHS. FastAPI's own _REQUEST_TELEMETRY_KEY follows the same pattern. Moving the key into an application-scoped object would require passing that object into record_auth_attributes, which runs inside a dependency, and that would add plumbing without changing behavior.

claude-opus-5-5

Comment thread app/telemetry.py
Comment on lines +62 to +64
span = trace.get_current_span()
if scope["type"] not in ("http", "websocket") or not span.is_recording():
await self.app(scope, receive, send)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bypass the middleware for excluded health probes

When /health or /health/ready is dispatched in-process while another recording span is current, FastAPI excludes the probe and therefore creates no request span, but this condition still accepts the inherited span as the probe's server span. The middleware then overwrites that unrelated span's client.address and exposes it through _SERVER_SPAN_KEY, so excluded probes can mutate their caller's telemetry; check the same health-probe exclusion before reading or tagging the current span.

AGENTS.md reference: AGENTS.md:L52-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd62cf1. ServerSpanMiddleware now applies the same health-probe check as native_telemetry_config() before it reads or tags the current span. A new unit test runs /health in-process under a caller span and asserts the caller span gets no client.address. It fails without the fix.

claude-opus-5-5

@Subhransu-De
Subhransu-De force-pushed the feat/fastapi-native-otel branch from e0f46da to b7543cb Compare October 11, 2026 06:17
FastAPI creates no request span for excluded probes, so an in-process
probe would tag whatever span its caller had open. Apply the same
health-probe check the native telemetry config uses.

Name the OpenTelemetry context key with a string literal instead of
calling create_key at import time.
@sonarqubecloud

Copy link
Copy Markdown

@Subhransu-De
Subhransu-De merged commit 9e85f62 into main Oct 11, 2026
37 checks passed
@Subhransu-De
Subhransu-De deleted the feat/fastapi-native-otel branch October 11, 2026 06:24
Subhransu-De added a commit that referenced this pull request Oct 11, 2026
)

A failed aiosqlite connect queues its worker-thread shutdown without
waiting for it (omnilib/aiosqlite#394). When the test's event loop closed
first, the thread raised "Event loop is closed", and pytest reported it
as an unhandled thread exception in the next test. This flaked test (3.13)
on main after #249.

Join the worker threads in the unreachable_settings fixture teardown,
off the event loop, so the loop is still alive when the thread finishes.
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