Optional OpenInference-conformant trace event module for the Cairnloop outbound lane (Phase 26, D-03).
Mirrors Cairnloop.Governance.Telemetry.Traces (Phase 17, D17-01 / D17-03) — same
architectural posture, swapped namespace + event vocabulary + attribution refs.
Namespace separation (D-03)
This module is SEPARATE from the bounded-metrics outbound paths emitted via
Cairnloop.Telemetry.span/3 + .execute/3. It emits to a disjoint 4-segment event
namespace:
[:cairnloop, :outbound, :trace, <event_atom>]The bounded-metrics outbound paths live at:
[:cairnloop, :outbound, :triggered, :start | :stop | :exception]
[:cairnloop, :outbound, :bulk, :triggered, :start | :stop | :exception]
[:cairnloop, :outbound, :bulk, :triggered]
[:cairnloop, :outbound, :delivery, :sent | :failed]No handler attached to any bounded-metrics path will receive trace events, and vice
versa. The two namespaces are disjoint by the :trace segment in position 3. This
isolation is proven in Cairnloop.Outbound.Telemetry.TracesTest.
Purpose
Hosts (and OI-aware consumers like Scoria, Phoenix.Tracer, OpenTelemetry exporters)
can attach to [:cairnloop, :outbound, :trace, ...] to reconstruct an outbound-lane
span tree (trigger lifecycle, bulk submit/refused, per-recipient delivery) without
duplicating durable record content.
Trace events are emitted alongside existing bounded-metrics events — they are observability only, never workflow truth (CLAUDE.md "telemetry is observability only").
Zero Scoria dependency
This module calls :telemetry.execute/3 directly. It imports no Scoria-owned
modules and does not route through Cairnloop.Telemetry (the bounded-metrics
centralizer). If no handler is attached, the event is silently dropped by the
:telemetry library — fail-closed.
OI span kinds (D-03 — execution-events-are-TOOL)
Trace events carry "openinference.span.kind" (string key per OI spec) in metadata:
- Execution events (
:delivery_sent,:delivery_failed):"TOOL" - All other outbound lifecycle events (trigger lifecycle, bulk lanes):
"GUARDRAIL"
Delivery IS the execution of an outbound message; lifecycle events (trigger started, trigger completed, bulk submitted, bulk refused, etc.) are guardrails around it.
Payload content exclusion (D-03 / mirrors D17-02)
Metadata carries only attribution references (IDs, atom enums). No rendered body, no message content, no refused-reason free-text ever crosses the telemetry boundary through this module.
Metadata SHAPE (always present):
"openinference.span.kind"— string key per OI spec:bulk_envelope_id— attribution ref (UUID string) ornil:conversation_id— attribution ref (integer) ornil(bulk-envelope-scoped events):template_id— attribution ref (string):actor_id— attribution ref (string) ornil(system-initiated events):outcome— atom enum
Metadata SHAPE (:bulk_refused only, per RESEARCH OQ3):
:effective_cap— integer (the cap-of-the-moment) for OI consumers correlating refusals against policy changes. Low cardinality, useful attribution detail.
Fail-closed guard clause
Unknown event atoms (anything not in @events) return :ok silently — emit/2
does not call :telemetry.execute/3. The caller is never penalised for passing
an atom not in the whitelist.
Summary
Functions
Emits an OI-conformant trace telemetry event for the outbound lane.
Functions
Emits an OI-conformant trace telemetry event for the outbound lane.
Only accepts events in @events (the 7-atom whitelist enumerated in the moduledoc).
Unknown events are silently dropped (fail-closed guard clause).
Event path: [:cairnloop, :outbound, :trace, event] — 4 segments with :trace in
position 3, disjoint from the bounded-metrics outbound paths (D-03).
Measurements are always %{count: 1} — callers do not supply measurements.
Metadata is built by build_metadata/2: attribution refs only, no payload content.