Flagsmith / Flagsmith/flagsmith
Observability: Bind request context to structlog via middleware
- Dominant language
- Python
- Stars
- 6.6k
- Forks
- 567
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 121
Description
The backend emits structured events via structlog from Django views and task handlers. Each emission today has to carry the identifiers it wants to expose — `organisation__id`, `user__id`, etc. — as explicit kwargs at the call site. That means either every emit site duplicates context-fetching code, or events lose the identifiers entirely when emitted from a deeply nested service where no org is in scope.
The new [events catalogue](https://docs.flagsmith.com/deployment-self-hosting/observability/events) makes the gap visible — several entries list sparse attribute sets because the surrounding code can't cheaply resolve context.
## Proposal
Add a Django middleware that runs after `AuthenticationMiddleware` and binds request context onto `structlog.contextvars` so every downstream emit in the request's scope picks it up automatically, without call-site changes.
Bindings:
- `user.id` — `request.user.uuid` when `request.user` is an `FFAdminUser`. Nothing bound for API-key principals (APIKeyUser has no uuid; a separate identifier would mix kinds).
- `organisation.id` — primary `request.user.key.organisation_id` (APIKeyUser via Master-API-Key), fallback `request.user.organisations.first().id` (FFAdminUser).
- `project.id` / `environment.id` — derived from URL kwargs (`project_pk`, `environment_api_key` → Environment lookup), bound during `process_view` so they're available for the view and anything downstream.
### Cleanup
Gunicorn's `gthread` worker (what we run with `GUNICORN_THREADS=2`) reuses long-lived threads across requests. Python's `contextvars` bindings persist on a thread's current context until explicitly cleared, so without cleanup, Request B on Thread 1 inherits Request A's bindings.
The middleware wraps the inner call in `try` / `finally clear_contextvars()` at the outer `__call__`. The `finally` runs even on view exceptions, so one request's bindings never reach the next one on the same thread. No race concerns across threads — each thread has its own context.
Pattern forward-compatible with ASGI / async views, since `contextvars` is the shared primitive.
## Out of scope
- SDK flag-eval routes (`X-Environment-Key` auth) have no user identity to bind. Environment / project bindings from URL path would still apply there.
- Frontend-provided baggage (e.g. Amplitude `device_id` / `session_id`) is already propagated via `W3CBaggagePropagator` and copied by `StructlogOTelProcessor`. No change needed.
- Propagating the bound context into task handlers enqueued during the request — separate, see Flagsmith/flagsmith-common#213.
## Acceptance
- Middleware binds the four contextvars above when derivable, clears at response.
- Existing emission sites (e.g. `code_references.scan.created`, `launch_darkly.import_failed`) drop their now-redundant `organisation__id` / `user__id` kwargs where the middleware covers them.
- Cross-request leakage test: two requests to the same worker thread, the second asserts no stale bindings from the first.
- The [events catalogue](https://docs.flagsmith.com/deployment-self-hosting/observability/events) gains a "Common attributes" preamble describing the globally-bound attributes — follow-up template tweak in flagsmith-common once the convention is settled.
Contributor guide
Assessment
This issue has not been assessed yet.