Statifier.Session.Telemetry (Statifier v2.0.0)

Copy Markdown View Source

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

EventMeasurementsMetadata
[:statifier, :session, :init]system_timesession_id, machine_name, trace, invoked_by, resumed
[:statifier, :session, :halt]macrostep, microstep, roundsession_id, reason, configuration
[:statifier, :session, :terminate]macrostep, microstep, roundsession_id, reason, status
[:statifier, :session, :macrostep, :start]system_time, monotonic_timesession_id, trigger, event_name, caller_context, span_ref
[:statifier, :session, :macrostep, :stop]duration, macrostep, microsteps, rounds, monotonic_timesession_id, trigger, outcome, event_name, configuration, caller_context, span_ref
[:statifier, :session, :interpret]effect_count, macrostep, microstepsession_id
[:statifier, :session, :unroutable]macrostep, microstepsession_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

EventMeasurementsMetadata
[:statifier, :session, :effect, :send]macrostep, microstep, roundsession_id, effect, location, send_id, target, c_index, owner
[:statifier, :session, :effect, :send_delayed]macrostep, microstep, round, delay_ms, ordinalsession_id, effect, location, send_id, target, c_index, owner, caller_context
[:statifier, :session, :effect, :cancel]macrostep, microstep, round, ordinalsession_id, effect, location, send_id, c_index, owner, caller_context
[:statifier, :session, :effect, :invoke]macrostep, microstep, roundsession_id, effect, location, invoke_id, state_index, invoke_index
[:statifier, :session, :effect, :cancel_invoke]macrostep, microstep, roundsession_id, effect, location, invoke_id, state_index
[:statifier, :session, :effect, :autoforward]macrostep, microstep, roundsession_id, effect, location, invoke_id, state_index
[:statifier, :session, :effect, :budget_exhausted]macrostep, microstep, round, budgetsession_id, effect, location
[:statifier, :session, :effect, :done]macrostep, microstep, roundsession_id, effect, location, configuration
[:statifier, :session, :effect, :log]macrostep, microstep, roundsession_id, effect, location, label, c_index, owner
[:statifier, :session, :effect, :datamodel_change]macrostep, microstep, roundsession_id, effect, location, location_path, location_source, new_value, prior_value, d_index, c_index, owner
[:statifier, :session, :effect, :datamodel_init]macrostep, microstep, roundsession_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.

EventMeasurementsMetadata
[:statifier, :session, :trace, :event_dequeued]macrostep, microstep, roundsession_id, effect
[:statifier, :session, :trace, :transitions_selected]macrostep, microstep, round, sizesession_id, effect
[:statifier, :session, :trace, :exit_set]macrostep, microstep, round, sizesession_id, effect
[:statifier, :session, :trace, :content_executed]macrostep, microstep, round, sizesession_id, effect
[:statifier, :session, :trace, :entry_set]macrostep, microstep, round, sizesession_id, effect
[:statifier, :session, :trace, :macrostep_stable]macrostep, microstep, roundsession_id, effect
[:statifier, :session, :trace, :done]macrostep, microstep, roundsession_id, effect, configuration
[:statifier, :session, :trace, :invoke_pass]macrostep, microstep, round, sizesession_id, effect
[:statifier, :session, :trace, :finalize_autoforward]macrostep, microstep, roundsession_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

Functions

effect(session_id, machine, arg)

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

events()

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

halt(session_id, reason, machine_state)

@spec halt(
  session_id :: String.t(),
  reason :: :done | :cancelled | :budget_exhausted,
  machine_state :: Statifier.MachineState.t()
) :: :ok

Emits [:statifier, :session, :halt].

init(session_id, machine, machine_state, invoked_by, resumed)

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

interpret(session_id, effect_count, machine_state)

@spec interpret(
  session_id :: String.t(),
  effect_count :: non_neg_integer(),
  machine_state :: Statifier.MachineState.t()
) :: :ok

Emits [:statifier, :session, :interpret].

macrostep_start(session_id, trigger, event, span_ref)

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

macrostep_stop(session_id, trigger, machine_state, event, outcome, start_time, span_ref)

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

terminate(session_id, reason, status, machine_state)

@spec terminate(
  session_id :: String.t(),
  reason :: term(),
  status :: term(),
  machine_state :: Statifier.MachineState.t()
) :: :ok

Emits [:statifier, :session, :terminate].

unroutable(session_id, machine, arg)

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