Audit forwarder that ships committed Sigra audit rows to Threadline
via Threadline.record_action/2.
Subscribes (in attach/1) to the [:sigra, :audit, :log] telemetry
event that fires only after a successful Repo.transaction/1 commit.
Sigra's audit_events table remains the authoritative source of truth —
Threadline is a post-commit projection, not a destination swap (Phase 131
boundary doctrine — D-21).
Dep-Off Safety (D-18, TL-04)
The entire defmodule is wrapped in if Code.ensure_loaded?(Threadline) do.
When :threadline is absent from mix.lock, this file compiles to a no-op
and the module simply does not exist. Sigra.Application.attach_forwarders/0
skips the attach call and emits one Logger.warning via
maybe_warn_missing_forwarder_deps/0 (D-23). Noop is NOT automatically
substituted — zero forwarding occurs in the degraded path.
Idempotency (RESEARCH.md §4 path 1, §7.2)
Each audit row's UUID is sent to Threadline as :correlation_id. Sigra UUIDs
are v4 random — collision probability is negligible — so the UUID alone
serves as the dedupe key. Recipe guides/recipes/companion-libs/threadline.md
documents the optional unique index on Threadline's audit_actions table
for strict Oban-retry idempotency.
Dispatch (D-10, D-11)
handle_event/4 reads the :dispatch option and routes accordingly:
:sync(or:autowhen Oban is not running) — callsThreadline.record_action/2inline in the calling process.:async(or:autowhen Oban is running) — enqueues aSigra.Workers.AuditForwardOban job via the shared dispatcher. The worker reloads the audit row by UUID and callshandle_event/4with:dispatch: :sync(forcing inline execution from the worker process).
The shared dispatcher (Sigra.Audit.Forwarders.dispatch/3) is used for
the :async path only — calling it for :sync would recurse because the
dispatcher's sync path calls handle_event/4. The inline Threadline call
is the correct sync implementation.
Attach Options (D-32)
Beyond the canonical keys (:id, :dispatch, :audit_schema, :repo,
:oban), this impl accepts:
:actor_type— atom identifying the default actor type passed toThreadline.Semantics.ActorRef.new/2whenactor_idis present in metadata. Defaults to:user.:threadline_module— module override for tests; defaults toThreadline.
Auto-Detach Landmine (D-20 — CRITICAL)
handle_event/4 wraps its entire body in try / rescue / catch :exit / catch :throw.
Every code path returns :ok to :telemetry — never :stop, never raises.
A handler that raises is auto-detached by :telemetry for the rest of BEAM
uptime (permanent silence). This is the worst failure mode in Phase 131.
Failure Events (D-29)
On any caught failure, emits [:sigra, :audit, :forward, :error] with
metadata.kind ∈ {:error, :exit, :throw} so operators can trace failures.
The originating audit transaction is already committed — forwarder failures
cannot roll it back (Pitfall 2, boundary doctrine D-21).