Sigra.Audit.Forwarders.Threadline (Sigra v1.0.0)

Copy Markdown View Source

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 :auto when Oban is not running) — calls Threadline.record_action/2 inline in the calling process.
  • :async (or :auto when Oban is running) — enqueues a Sigra.Workers.AuditForward Oban job via the shared dispatcher. The worker reloads the audit row by UUID and calls handle_event/4 with :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 to Threadline.Semantics.ActorRef.new/2 when actor_id is present in metadata. Defaults to :user.
  • :threadline_module — module override for tests; defaults to Threadline.

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).

Summary

Functions

handle_event(event, measurements, metadata, opts)