The :telemetry bridge (ADR-0040) - the single authoritative reference for
every [:statifier, :session, ...] event Statifier.Session can emit.
Statifier.Session is the ADR-0003 effect interpreter; this module is the
emission half of that role, split out only because .doctor.exs's 100%
Doctor bar puts the event contract in a @moduledoc a consumer can read
without opening the source (ADR-0040's "the session boundary is two
files"). It holds no state, drives no core function, and is called from
nowhere but Statifier.Session.
events/0 returns every name below, built from the same literal-atom lists
the emitters use, so a consumer can :telemetry.attach_many/4 to the whole
surface without hand-copying names out of this table.
Measurements are numbers, metadata is everything else
Every measurement is a number - :telemetry_metrics-aggregatable. Every
identity - a session id, an effect struct, a resolved location, and every
constraint-3 index (state_index, t_index, c_index, invoke_index) -
is metadata, including the ones that happen to be integers: an opaque
index has no numeric meaning to average.
A configuration in metadata (on :halt, on the :macrostep, :stop event,
and on the :effect, :done/:trace, :done events below) is translated
from the raw MapSet.t(non_neg_integer()) the machine state or the
Done/Trace.Done payload's own configuration field carries into a
MapSet.t(String.t()) of state ids via Statifier.Machine.id/2, so a
subscriber never needs a Machine handle to read a configuration out of an
event - including the terminal one, where MachineState.configuration is
already empty and the payload's own field is the only carrier left.
Span correlation
The macrostep span's start and stop halves both carry span_ref, a
make_ref/0 reference generated once per span and passed unchanged from
macrostep_start/4 to the matching macrostep_stop/7 - the
telemetry_span_context convention :telemetry.span/3 uses, so a
subscriber (including an OTel bridge) pairs a span's two halves on
span_ref rather than on (session_id, macrostep), which is unusable for
this because macrostep is not stable across the span (below) and ADR-0039
re-entry can nest an :internal span inside an :event span already in
flight, producing start, start, stop, stop with no other way to tell
which start belongs to which stop. Each span - including a nested one - gets
its own span_ref. Both halves also carry monotonic_time
(System.monotonic_time/0), matching :telemetry_metrics/OTel convention.
The start half deliberately carries no macrostep. The counter this span
opens against advances inside the core call the span brackets
(MachineState.begin_macrostep/1, called from Interpreter.handle_event/2
among others), so a macrostep read at the start half can be the
pre-increment value while the stop half reports the post-increment one -
the two would silently disagree on a pairing key. span_ref is the
correlation mechanism; the stop half is the only place macrostep is
authoritative for this span.
This bridge hand-rolls the span pair rather than calling
:telemetry.span/3: that helper wraps its span function in a rescue, so
it always emits a matching :exception event, and this bridge has no
:exception half to offer (below) - forwarding decisions the session's
perform/3 already made deterministically, not evaluating one under a
rescue of its own would invent a failure mode the session does not have.
Two consumer-facing caveats
There is deliberately no fourth, :exception-named member of the
:macrostep span family alongside :start/:stop (previous section): a
crash mid-span - inside perform/3, say - leaves that span's start
unmatched by any stop. A subscriber pairing on span_ref should not expect
every span_ref it sees on a start to arrive again on a stop.
The :initialize span's start event is emitted after
Interpreter.initialize/2 has already returned (Statifier.Session.init/1
needs the machine_state that call produces before it can call
Statifier.Session.Telemetry.init/5, which happens first), so its
system_time is not the wall-clock instant the span actually opened at,
even though duration on the matching stop is still measured from a
System.monotonic_time/0 reading taken before that call.
Locations are resolved at emission, for single-index effects only
An effect carrying exactly one resolvable index gets metadata.location,
resolved through Statifier.Machine.content/2, Statifier.Machine.at/2,
Statifier.Machine.transition/2, or Statifier.Machine.data/2 as
appropriate, and carried as the %Statifier.Parser.Location{} struct
verbatim. Where no index resolves - an
effect with no index field at all, or an index field present but nil -
the two families answer differently, and the difference is the contract, not
an inconsistency to smooth over. A core effect still carries the key,
resolving to location: nil: every core_shape/2 clause sets it
unconditionally, so metadata.location is safe to read without a
Map.has_key?/2 guard. A trace effect omits the key entirely - there, a
key that can never hold a value is contract noise a consumer would otherwise
have to learn to ignore, so it is absent rather than present and nil
(ADR-0040 and its amendments). An effect carrying a list of
indexes carries the list in metadata
(nested in the effect struct) and its length as a size measurement,
with no location key, at any cardinality - zero, one, or many entries.
No [:statifier, :session, :trace, _] event ever carries a location
key, because every trace effect carrying a resolvable index carries it as
a list: a set-valued trace event names a phase (an exit set, an entry
set, an invoke-pass walk, a selection round's result), not a chart
element, so a location resolved from a list that happens to hold one
entry would describe a coincidence of the chart and the round rather than
a property of the event, and the rule would need a per-event footnote the
moment a second list field entered the picture (Trace.InvokePass
already carries two). Locations on this surface live exclusively on the
single-index core effect events and :unroutable, resolved by the rule
above (ADR-0040 Decision 4, amended). cond_location is never
resolved: no effect in the vocabulary is emitted from guard evaluation.
Lifecycle and span events (7), emitted regardless of trace
| Event | Measurements | Metadata |
|---|---|---|
[:statifier, :session, :init] | system_time | session_id, machine_name, trace, invoked_by, resumed |
[:statifier, :session, :halt] | macrostep, microstep, round | session_id, reason, configuration |
[:statifier, :session, :terminate] | macrostep, microstep, round | session_id, reason, status |
[:statifier, :session, :macrostep, :start] | system_time, monotonic_time | session_id, trigger, event_name, caller_context, span_ref |
[:statifier, :session, :macrostep, :stop] | duration, macrostep, microsteps, rounds, monotonic_time | session_id, trigger, outcome, event_name, configuration, caller_context, span_ref |
[:statifier, :session, :interpret] | effect_count, macrostep, microstep | session_id |
[:statifier, :session, :unroutable] | macrostep, microstep | session_id, effect, target, send_id, location |
[:statifier, :session, :unroutable] names a routing failure the session
itself detected (the {:unroutable, effect} instruction), not an effect
the core produced - a consumer alerting on it wants one name to attach to
rather than a filter across the nine effect names below.
[:statifier, :session, :terminate] fires from terminate/2 and is
therefore subject to the GenServer contract: it does not fire on a brutal
kill. :halt is the event to build a "session finished" metric on.
duration (on :macrostep, :stop) is :native units, per :telemetry's
own convention - unit conversion (System.convert_time_unit/3) is left to
the consumer.
caller_context (on both macrostep halves here, and on the
:send_delayed/:cancel effect events below) is ADR-0063's opaque host
slot: the triggering external event's caller_context, nil for the
:initialize/:cancel/:internal/:resume triggers and for an event
sent without one. Identity, never a number, so it is metadata everywhere
and a measurement nowhere (ADR-0040's split). Both halves carry it for
the same reason both carry trigger and event_name: a consumer
attaching to only one half still attributes. On the two effect events
the value also rides in metadata.effect verbatim; the explicit key
keeps a bridge's read uniform with the macrostep events. A consumer
treats the term as opaque - something to parent or link with, never to
flatten into exported attributes.
Core effect events (11), emitted regardless of trace
| Event | Measurements | Metadata |
|---|---|---|
[:statifier, :session, :effect, :send] | macrostep, microstep, round | session_id, effect, location, send_id, target, c_index, owner |
[:statifier, :session, :effect, :send_delayed] | macrostep, microstep, round, delay_ms, ordinal | session_id, effect, location, send_id, target, c_index, owner, caller_context |
[:statifier, :session, :effect, :cancel] | macrostep, microstep, round, ordinal | session_id, effect, location, send_id, c_index, owner, caller_context |
[:statifier, :session, :effect, :invoke] | macrostep, microstep, round | session_id, effect, location, invoke_id, state_index, invoke_index |
[:statifier, :session, :effect, :cancel_invoke] | macrostep, microstep, round | session_id, effect, location, invoke_id, state_index |
[:statifier, :session, :effect, :autoforward] | macrostep, microstep, round | session_id, effect, location, invoke_id, state_index |
[:statifier, :session, :effect, :budget_exhausted] | macrostep, microstep, round, budget | session_id, effect, location |
[:statifier, :session, :effect, :done] | macrostep, microstep, round | session_id, effect, location, configuration |
[:statifier, :session, :effect, :log] | macrostep, microstep, round | session_id, effect, location, label, c_index, owner |
[:statifier, :session, :effect, :datamodel_change] | macrostep, microstep, round | session_id, effect, location, location_path, location_source, new_value, prior_value, d_index, c_index, owner |
[:statifier, :session, :effect, :datamodel_init] | macrostep, microstep, round | session_id, effect, location, datamodel |
Trace effect events (9), emitted only under trace: true
The trace family vanishes in the core, not at this bridge:
Statifier.Effect.trace/3 expands to no effect at all when
machine_state.trace is false, so there is nothing here to forward.
This module contains no if trace of its own.
| Event | Measurements | Metadata |
|---|---|---|
[:statifier, :session, :trace, :event_dequeued] | macrostep, microstep, round | session_id, effect |
[:statifier, :session, :trace, :transitions_selected] | macrostep, microstep, round, size | session_id, effect |
[:statifier, :session, :trace, :exit_set] | macrostep, microstep, round, size | session_id, effect |
[:statifier, :session, :trace, :content_executed] | macrostep, microstep, round, size | session_id, effect |
[:statifier, :session, :trace, :entry_set] | macrostep, microstep, round, size | session_id, effect |
[:statifier, :session, :trace, :macrostep_stable] | macrostep, microstep, round | session_id, effect |
[:statifier, :session, :trace, :done] | macrostep, microstep, round | session_id, effect, configuration |
[:statifier, :session, :trace, :invoke_pass] | macrostep, microstep, round, size | session_id, effect |
[:statifier, :session, :trace, :finalize_autoforward] | macrostep, microstep, round | session_id, effect |
kind (the fourth segment) is derived by a private, multi-clause function
pattern-matching each Statifier.Effect.Trace.* struct to a literal atom -
never Module.split/1 composed with String.to_atom/1, which
Credo.Check.Warning.UnsafeToAtom forbids and which would make this
module's event list impossible to enumerate ahead of a call.
Summary
Types
One of the eleven core effect payload structs (Statifier.Effect.core/0, unwrapped).
One :telemetry event name this module can emit.
One of the nine trace payload structs (Statifier.Effect.trace/0, unwrapped).
Functions
Emits [:statifier, :session, :effect, kind] or
[:statifier, :session, :trace, kind], dispatching on effect's own tag.
machine resolves location per the moduledoc's single-index rule.
Every event name this module can ever emit - the 7 lifecycle/span names,
the 11 [:statifier, :session, :effect, kind] names, and the 9
[:statifier, :session, :trace, kind] names, built from
@lifecycle_events/@effect_kinds/@trace_kinds, the module's single
definition site for the vocabulary.
Emits [:statifier, :session, :halt].
Emits [:statifier, :session, :init]. resumed is ADR-0060's
metadatum - true when this session booted from a :resume option
rather than running Statifier.Interpreter.initialize/2. Additive: an
existing handler reading only the four original keys is unaffected by the
fifth.
Emits [:statifier, :session, :interpret].
Emits [:statifier, :session, :macrostep, :start]. span_ref is a
make_ref/0 reference generated once by the caller and passed unchanged
to the matching macrostep_stop/7 - the pairing mechanism a subscriber
needs (Decision 2 below), since macrostep cannot serve that role here:
the counter this span opens against advances inside the core call the
span brackets, so the start half is emitted before the increment and the
stop half after it. Carrying the pre-increment value on start would look
paired with the wrong stop on every span whose trigger's core call
advances the counter (:event, and any other trigger with the same
shape); carrying the post-increment value on start would require reading
it out of a machine state the start half does not otherwise need. Rather
than pick one and leave the other case's start event silently wrong, the
start half carries no macrostep at all - span_ref is the only
correlation mechanism, and it is exact by construction.
Emits [:statifier, :session, :macrostep, :stop]. start_time is a
System.monotonic_time/0 reading taken at the matching
macrostep_start/4; duration is the difference, in :native units.
span_ref is the same reference passed to that macrostep_start/4 call,
carried again here so a subscriber can pair this stop with its start
(Decision 2) - nested spans (an :internal span opened by ADR-0039
re-entry inside an :event span already in flight) each get their own
reference, so start, start, stop, stop is disambiguated by span_ref
rather than by call order.
Emits [:statifier, :session, :terminate].
Emits [:statifier, :session, :unroutable] for an effect the session could not route.
Types
@type core_payload() :: Statifier.Effect.Send.t() | Statifier.Effect.SendDelayed.t() | Statifier.Effect.Cancel.t() | Statifier.Effect.Invoke.t() | Statifier.Effect.CancelInvoke.t() | Statifier.Effect.Autoforward.t() | Statifier.Effect.BudgetExhausted.t() | Statifier.Effect.Done.t() | Statifier.Effect.Log.t() | Statifier.Effect.DatamodelChange.t() | Statifier.Effect.DatamodelInit.t()
One of the eleven core effect payload structs (Statifier.Effect.core/0, unwrapped).
@type event_name() :: [atom(), ...]
One :telemetry event name this module can emit.
@type trace_payload() :: Statifier.Effect.Trace.EventDequeued.t() | Statifier.Effect.Trace.TransitionsSelected.t() | Statifier.Effect.Trace.ExitSet.t() | Statifier.Effect.Trace.ContentExecuted.t() | Statifier.Effect.Trace.EntrySet.t() | Statifier.Effect.Trace.MacrostepStable.t() | Statifier.Effect.Trace.Done.t() | Statifier.Effect.Trace.InvokePass.t() | Statifier.Effect.Trace.FinalizeAutoforward.t()
One of the nine trace payload structs (Statifier.Effect.trace/0, unwrapped).
Functions
@spec effect( session_id :: String.t(), machine :: Statifier.Machine.t(), effect :: Statifier.Effect.t() ) :: :ok
Emits [:statifier, :session, :effect, kind] or
[:statifier, :session, :trace, kind], dispatching on effect's own tag.
machine resolves location per the moduledoc's single-index rule.
@spec events() :: [event_name()]
Every event name this module can ever emit - the 7 lifecycle/span names,
the 11 [:statifier, :session, :effect, kind] names, and the 9
[:statifier, :session, :trace, kind] names, built from
@lifecycle_events/@effect_kinds/@trace_kinds, the module's single
definition site for the vocabulary.
@spec halt( session_id :: String.t(), reason :: :done | :cancelled | :budget_exhausted, machine_state :: Statifier.MachineState.t() ) :: :ok
Emits [:statifier, :session, :halt].
@spec init( session_id :: String.t(), machine :: Statifier.Machine.t(), machine_state :: Statifier.MachineState.t(), invoked_by :: {pid(), String.t()} | nil, resumed :: boolean() ) :: :ok
Emits [:statifier, :session, :init]. resumed is ADR-0060's
metadatum - true when this session booted from a :resume option
rather than running Statifier.Interpreter.initialize/2. Additive: an
existing handler reading only the four original keys is unaffected by the
fifth.
@spec interpret( session_id :: String.t(), effect_count :: non_neg_integer(), machine_state :: Statifier.MachineState.t() ) :: :ok
Emits [:statifier, :session, :interpret].
@spec macrostep_start( session_id :: String.t(), trigger :: :initialize | :event | :cancel | :internal | :resume, event :: Statifier.Event.t() | nil, span_ref :: reference() ) :: :ok
Emits [:statifier, :session, :macrostep, :start]. span_ref is a
make_ref/0 reference generated once by the caller and passed unchanged
to the matching macrostep_stop/7 - the pairing mechanism a subscriber
needs (Decision 2 below), since macrostep cannot serve that role here:
the counter this span opens against advances inside the core call the
span brackets, so the start half is emitted before the increment and the
stop half after it. Carrying the pre-increment value on start would look
paired with the wrong stop on every span whose trigger's core call
advances the counter (:event, and any other trigger with the same
shape); carrying the post-increment value on start would require reading
it out of a machine state the start half does not otherwise need. Rather
than pick one and leave the other case's start event silently wrong, the
start half carries no macrostep at all - span_ref is the only
correlation mechanism, and it is exact by construction.
@spec macrostep_stop( session_id :: String.t(), trigger :: :initialize | :event | :cancel | :internal | :resume, machine_state :: Statifier.MachineState.t(), event :: Statifier.Event.t() | nil, outcome :: :quiescent | :done | :cancelled | :budget_exhausted, start_time :: integer(), span_ref :: reference() ) :: :ok
Emits [:statifier, :session, :macrostep, :stop]. start_time is a
System.monotonic_time/0 reading taken at the matching
macrostep_start/4; duration is the difference, in :native units.
span_ref is the same reference passed to that macrostep_start/4 call,
carried again here so a subscriber can pair this stop with its start
(Decision 2) - nested spans (an :internal span opened by ADR-0039
re-entry inside an :event span already in flight) each get their own
reference, so start, start, stop, stop is disambiguated by span_ref
rather than by call order.
@spec terminate( session_id :: String.t(), reason :: term(), status :: term(), machine_state :: Statifier.MachineState.t() ) :: :ok
Emits [:statifier, :session, :terminate].
@spec unroutable( session_id :: String.t(), machine :: Statifier.Machine.t(), effect :: Statifier.Effect.t() ) :: :ok
Emits [:statifier, :session, :unroutable] for an effect the session could not route.