Flagsmith / Flagsmith/flagsmith

Observability: Bind request context to structlog via middleware

Open
#7,298 1 comment 0 reactions 1 assignee Claimed by @emyller View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.