Plug that extracts AuditContext from a Plug.Conn and stores it in
conn.assigns[:audit_context].
Usage
# In a Phoenix router pipeline or endpoint:
plug Threadline.Plug
# With actor extraction:
plug Threadline.Plug, actor_fn: &MyApp.Auth.to_actor_ref/1
# With additive request-context overrides:
plug Threadline.Plug,
actor_fn: &MyApp.Auth.to_actor_ref/1,
context_overrides_fn: &MyApp.Auth.audit_context_overrides/1Options
:actor_fn— a function(Plug.Conn.t() -> ActorRef.t() | nil)that extracts the current actor from the conn. Called duringcall/2. If omitted,audit_context.actor_refwill be nil.:context_overrides_fn— a function(Plug.Conn.t() -> map())that returns additive:request_idand:correlation_idvalues only.Threadline.Plugderives those values from headers and conn state first; this callback can only fill missing values and cannot replace actor identity orremote_ip. Unknown keys and non-map returns raiseArgumentError.
What is extracted
actor_ref— result of:actor_fn(or nil)request_id— fromx-request-idheader, thenconn.assigns[:request_id], then nilcorrelation_id— fromx-correlation-idheader, or nilremote_ip— fromconn.remote_ip, formatted as a dotted-decimal string
When :context_overrides_fn is configured, its return value is merged after
baseline extraction. Explicit header or conn-derived request_id and
correlation_id values always win; override values only fill missing fields.
Hosts that need proxy-aware IP handling should normalize conn.remote_ip
upstream before Threadline.Plug runs.
PgBouncer note
This Plug does not call SET / SET LOCAL on the database connection. Request
metadata lives on conn.assigns only. This design is safe for PgBouncer
transaction-mode pooling.
PostgreSQL bridge (CTX-03)
To populate audit_transactions.actor_ref from capture triggers, the host
must set a transaction-local GUC inside the same Ecto.Repo.transaction/1
as audited writes, before the first row change in that transaction:
json = Threadline.Semantics.ActorRef.to_map(actor_ref) |> Jason.encode!()
Repo.transaction(fn ->
Repo.query!("SELECT set_config('threadline.actor_ref', $1::text, true)", [json])
# ... audited writes here ...
end)The trigger reads threadline.actor_ref via current_setting only; it never
calls set_config itself (see gate-01-01.md). See
test/threadline/capture/trigger_context_test.exs for the contract example.