Skip to content

Observability: Bind request context to structlog via middleware #7298

@khvn26

Description

@khvn26

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 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.idrequest.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 Task Processor: Propagate structlog contextvars across task runs 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 gains a "Common attributes" preamble describing the globally-bound attributes — follow-up template tweak in flagsmith-common once the convention is settled.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions