StatifierOban.Telemetry (StatifierOban v0.9.1)

Copy Markdown View Source

The :telemetry surface for this package's durable seams (ADR-0006) - the single definition site for every [:statifier_oban, ...] event name, and the one module that calls :telemetry.execute/3.

docs/telemetry.md is the full contract: what each event answers, what it deliberately leaves to Oban and to statifier-ex, and what opentelemetry_statifier does with it. This moduledoc is the reference table; that note is the reasoning.

events/0 returns every name below, built from the same literal-atom lists the emitters use, so the bridge can attach one handler per event name without hand-copying the list (ADR-0006 decision 4, ots-ADR-0003).

What these events are for

Two other surfaces already report on this package's work. Oban instruments the job ([:oban, :job, :start | :stop | :exception], [:oban, :engine, ...]), and statifier-ex instruments the effect ([:statifier, :session, :effect, :send_delayed | :cancel | :invoke]). What neither can see is the durable step between them, and that is the whole of what this module emits: whether the effect became a row and whether the insert was new (conflict?), which statechart identity an opaque job row belongs to, and the spec-level verdicts that are successes for Oban and non-events for the chart (the 6.2 discard, the 6.3/6.4.3 sweeps, a permanently failed invocation).

Duration, attempts, retries, snoozes, queue latency and exceptions are Oban's and are not re-emitted; in particular there is no lateness measurement here, because :queue_time on [:oban, :job, :stop] is the same subtraction against the same two timestamps.

Structural rules (ADR-0006 decisions 3, 4, 5, 8)

  • The prefix is [:statifier_oban, ...], fixed and not configurable. Upstream reserves its own second segment (:session) for the logical SCXML session, and nothing here is scoped to one.
  • Measurements are numbers; metadata is everything else, integer indexes included - ordinal, macrostep, microstep and round are metadata here, because an opaque index has no numeric meaning to average.
  • Every event is a single point-in-time event. There are no :start/:stop pairs: this package owns no interval Oban does not already own, so every event measures system_time plus whatever numbers are genuinely its own.
  • Emission is unconditional. There is no config knob and no sampling knob: :telemetry.execute/3 on an event with no handlers is a lookup and a return.
  • Amendment discipline. Adding a measurement or a metadata key to an existing event is an amendment and is fine; renaming or removing one, or renaming an event, is breaking and needs a new ADR.

Scheduling seam

Emitted synchronously on the process that drove the macrostep, before any Oban job exists.

EventMeasurementsMetadata
[:statifier_oban, :timer, :scheduled]system_time, delay_msscope, send_id, ordinal, macrostep, microstep, round, scheduled_at, queue, conflict?, job_id, caller_context
[:statifier_oban, :timer, :schedule_rejected]system_timescope, send_id, ordinal, reason
[:statifier_oban, :timer, :cancelled]system_time, countscope, send_id, ordinal, caller_context
[:statifier_oban, :invoke, :enqueued]system_timescope, invoke_id, macrostep, handler, queue, conflict?, job_id
[:statifier_oban, :invoke, :enqueue_rejected]system_timescope, invoke_id, handler, reason
[:statifier_oban, :invoke, :cancelled]system_time, countscope, invoke_id, handler

conflict? is what answers "did that replay do anything": true means the dedup key held and no second job was stored.

count on the two cancellation events is the sweep's own return, and 0 is data rather than an error - spec 6.3 cancels every timer under a send_id, and a cancel that matches nothing is a no-op.

reason on the two *_rejected events is the typed error the call returned, unchanged. scope is nil on [:statifier_oban, :invoke, :enqueue_rejected] when the rejection is {:invalid_scope, ctx} itself: there is no validated scope to report, and reporting the raw ctx would put unvalidated host state on the event.

Delivery seam

Emitted on the Oban worker process, inside the job.

EventMeasurementsMetadata
[:statifier_oban, :timer, :fired]system_time, attemptscope, send_id, ordinal, delivery, job_id, caller_context
[:statifier_oban, :timer, :discarded]system_time, attemptscope, send_id, ordinal, delivery, reason, job_id, caller_context
[:statifier_oban, :invoke, :delivered]system_time, attemptscope, invoke_id, macrostep, handler, delivery, job_id
[:statifier_oban, :invoke, :discarded]system_time, attemptscope, invoke_id, macrostep, handler, delivery, reason, job_id
[:statifier_oban, :invoke, :failed]system_time, attemptsscope, invoke_id, reason, detail, handler, job_id

reason on the two :discarded events is the delivery seam's StatifierOban.Timer.Delivery.discard_reason/0 - the spec 6.2 verdict as data, which Oban buries inside :result.

[:statifier_oban, :invoke, :failed] mirrors StatifierOban.Invoke.Delivery.deliver_failure/3 exactly, including ADR-0005's three-class reason vocabulary ("run_failed", "run_crashed", "undecodable") and its attempts semantics. Its handler is nil on the "undecodable" class alone: that row never reached handler resolution, because the decode that would have named the module is the thing that failed.

Where the seam delivers nothing, this emits nothing: the environment errors :invalid_handler, :invalid_delivery, :invalid_codec and :codec_failed say the deploy is wrong rather than anything about the invocation, and they ride Oban's exception event.

Fan-out seam

Emitted around ADR-0007's fan-out, and recorded by ADR-0006's 2026-09-06 amendment. None of the three is an answer: the fan-out job completes without delivering, a child start creates a run and delivers nothing, and the settlement side answers the invocation once on behalf of all N.

EventMeasurementsMetadata
[:statifier_oban, :invoke, :fan_out]system_time, countscope, invoke_id, handler, policy, queue, job_id, caller_context
[:statifier_oban, :invoke, :child_started]system_time, attemptscope, invoke_id, index, count, job_id, caller_context
[:statifier_oban, :invoke, :unstarted_cancelled]system_time, countscope, invoke_id

count is the fan-out's width on all three. It is a measurement on :fan_out and :unstarted_cancelled, where it is the number the call itself produced - the status it already has on the two :cancelled events, 0 included - and metadata on :child_started, where index and count together are the child's position rather than a quantity.

:unstarted_cancelled's count is half of sb-ADR-0009 decision 6's first_error cancel: the indices whose start job never ran. The siblings that already have a child run are the settlement side's to cancel and are not counted here.

handler is on :fan_out alone - ChildStartWorker never resolves the handler name to a module and cancel_unstarted/3 is given no handler at all, and a string where every other event carries a module would be worse than the absence.

Cardinality

Every metadata key above is bounded by the chart rather than by traffic, except job_id, which is a correlation id for a span or a log line and never a metric dimension, and caller_context, which is an opaque host term present for the bridge alone.

Nothing from the chart's datamodel is ever on an event. The effect's data, params and content are host-opaque terms (StatifierOban.OpaqueTerm), possibly encrypted through a host codec, and they are never emitted - not truncated, not hashed, not "just the keys".

Summary

Types

One :telemetry event name this module can emit.

The identity a job row is keyed under - see StatifierOban.Timer.Key.

Functions

Every event name this module can ever emit - the 5 [:statifier_oban, :timer, kind] names and the 9 [:statifier_oban, :invoke, kind] names, built from @timer_kinds and @invoke_kinds, this module's single definition site for the vocabulary.

Emits [:statifier_oban, :invoke, :cancelled] - the spec 6.4.3 sweep ran and cancelled count stored jobs across every generation. count: 0 is a no-op, not an error.

Emits [:statifier_oban, :invoke, :child_started] - the host's StatifierOban.Invoke.ChildStarter.start_child/5 seam created the child at index of count.

Emits [:statifier_oban, :invoke, :delivered] - run/1 (or run/2) completed and the seam fed done.invoke.<invoke_id> into a live run.

Emits [:statifier_oban, :invoke, :discarded] - a completed invocation landed on a run that is no longer live, dropped the same way a fired timer is.

Emits [:statifier_oban, :invoke, :enqueue_rejected] - no row was stored. scope is nil when the rejection is the scope validation itself.

Emits [:statifier_oban, :invoke, :enqueued] - the durable write for one %Statifier.Effect.Invoke{} happened, and conflict? says whether it was new.

Emits [:statifier_oban, :invoke, :failed] - the terminal attempt gave up and error.communication.invoke.<invoke_id> went into the run.

Emits [:statifier_oban, :invoke, :fan_out] - one invocation became count durable child starts rather than an answer, and every start is stored.

Emits [:statifier_oban, :invoke, :unstarted_cancelled] - the unstarted half of sb-ADR-0009 decision 6's first_error cancel ran and cancelled count start jobs. count: 0 is a no-op, not an error, and the siblings that already have a child run are cancelled by the settlement side and are not counted here.

Emits [:statifier_oban, :timer, :cancelled] - the spec 6.3 sweep ran and cancelled count stored jobs. count: 0 is a no-op, not an error.

Emits [:statifier_oban, :timer, :discarded] - the spec 6.2 drop, as data. reason is the delivery seam's own StatifierOban.Timer.Delivery.discard_reason/0.

Emits [:statifier_oban, :timer, :fired] - the delivery seam fed the event back into a live run.

Emits [:statifier_oban, :timer, :schedule_rejected] - no row was stored. reason is StatifierOban.Timer.schedule_error/0, and the commonest value, {:non_self_target, target}, is the st-ADR-0055 bailout rather than a fault.

Emits [:statifier_oban, :timer, :scheduled] - the durable write for one %Statifier.Effect.SendDelayed{} happened, and conflict? says whether it was new.

Types

event_name()

@type event_name() :: [atom(), ...]

One :telemetry event name this module can emit.

scope()

@type scope() :: String.t()

The identity a job row is keyed under - see StatifierOban.Timer.Key.

Functions

events()

@spec events() :: [event_name()]

Every event name this module can ever emit - the 5 [:statifier_oban, :timer, kind] names and the 9 [:statifier_oban, :invoke, kind] names, built from @timer_kinds and @invoke_kinds, this module's single definition site for the vocabulary.

The list is what opentelemetry_statifier attaches to, one handler per name (ots-ADR-0003), so it is as public as a function signature.

invoke_cancelled(scope, handler, invoke_id, count)

@spec invoke_cancelled(scope(), module(), String.t(), non_neg_integer()) :: :ok

Emits [:statifier_oban, :invoke, :cancelled] - the spec 6.4.3 sweep ran and cancelled count stored jobs across every generation. count: 0 is a no-op, not an error.

invoke_child_started(scope, invoke, index, count, job)

@spec invoke_child_started(
  scope(),
  Statifier.Effect.Invoke.t(),
  non_neg_integer(),
  pos_integer(),
  Oban.Job.t()
) :: :ok

Emits [:statifier_oban, :invoke, :child_started] - the host's StatifierOban.Invoke.ChildStarter.start_child/5 seam created the child at index of count.

A child start is not an answer (ADR-0007): nothing was delivered into the run. index and count are the child's position and ride as metadata; attempt is the start job's own, so a child created on a retry is distinguishable from one created first time.

invoke_delivered(scope, handler, invoke, delivery, job)

@spec invoke_delivered(
  scope(),
  module(),
  Statifier.Effect.Invoke.t(),
  module(),
  Oban.Job.t()
) :: :ok

Emits [:statifier_oban, :invoke, :delivered] - run/1 (or run/2) completed and the seam fed done.invoke.<invoke_id> into a live run.

invoke_discarded(scope, handler, invoke, delivery, reason, job)

@spec invoke_discarded(
  scope(),
  module(),
  Statifier.Effect.Invoke.t(),
  module(),
  term(),
  Oban.Job.t()
) ::
  :ok

Emits [:statifier_oban, :invoke, :discarded] - a completed invocation landed on a run that is no longer live, dropped the same way a fired timer is.

invoke_enqueue_rejected(scope, handler, invoke, reason)

@spec invoke_enqueue_rejected(
  scope() | nil,
  module(),
  Statifier.Effect.Invoke.t(),
  term()
) :: :ok

Emits [:statifier_oban, :invoke, :enqueue_rejected] - no row was stored. scope is nil when the rejection is the scope validation itself.

invoke_enqueued(scope, handler, invoke, job)

@spec invoke_enqueued(scope(), module(), Statifier.Effect.Invoke.t(), Oban.Job.t()) ::
  :ok

Emits [:statifier_oban, :invoke, :enqueued] - the durable write for one %Statifier.Effect.Invoke{} happened, and conflict? says whether it was new.

invoke_failed(scope, handler, invoke_id, reason, detail, attempts, job_id)

@spec invoke_failed(
  scope(),
  module() | nil,
  String.t(),
  String.t(),
  String.t(),
  pos_integer(),
  term()
) :: :ok

Emits [:statifier_oban, :invoke, :failed] - the terminal attempt gave up and error.communication.invoke.<invoke_id> went into the run.

Mirrors StatifierOban.Invoke.Delivery.deliver_failure/3: reason is ADR-0005's three-class vocabulary and attempts follows its semantics - max_attempts for the two run/1 classes, the cancelling attempt's own number for "undecodable". handler is nil for "undecodable", where the row never reached handler resolution.

invoke_fan_out(scope, handler, invoke, count, policy, queue, job)

@spec invoke_fan_out(
  scope(),
  module(),
  Statifier.Effect.Invoke.t(),
  non_neg_integer(),
  atom(),
  atom() | String.t(),
  Oban.Job.t()
) :: :ok

Emits [:statifier_oban, :invoke, :fan_out] - one invocation became count durable child starts rather than an answer, and every start is stored.

policy and queue are StatifierOban.Invoke.FanOut.start/5's own - the aggregation the children were actually enqueued under, and the queue they went to - rather than a second reading of the invocation.

invoke_unstarted_cancelled(scope, invoke_id, count)

@spec invoke_unstarted_cancelled(scope(), String.t(), non_neg_integer()) :: :ok

Emits [:statifier_oban, :invoke, :unstarted_cancelled] - the unstarted half of sb-ADR-0009 decision 6's first_error cancel ran and cancelled count start jobs. count: 0 is a no-op, not an error, and the siblings that already have a child run are cancelled by the settlement side and are not counted here.

timer_cancelled(scope, effect, count)

@spec timer_cancelled(scope(), Statifier.Effect.Cancel.t(), non_neg_integer()) :: :ok

Emits [:statifier_oban, :timer, :cancelled] - the spec 6.3 sweep ran and cancelled count stored jobs. count: 0 is a no-op, not an error.

timer_discarded(scope, effect, delivery, reason, job)

@spec timer_discarded(
  scope(),
  Statifier.Effect.SendDelayed.t(),
  module(),
  term(),
  Oban.Job.t()
) :: :ok

Emits [:statifier_oban, :timer, :discarded] - the spec 6.2 drop, as data. reason is the delivery seam's own StatifierOban.Timer.Delivery.discard_reason/0.

timer_fired(scope, effect, delivery, job)

@spec timer_fired(scope(), Statifier.Effect.SendDelayed.t(), module(), Oban.Job.t()) ::
  :ok

Emits [:statifier_oban, :timer, :fired] - the delivery seam fed the event back into a live run.

timer_schedule_rejected(scope, effect, reason)

@spec timer_schedule_rejected(scope(), Statifier.Effect.SendDelayed.t(), term()) ::
  :ok

Emits [:statifier_oban, :timer, :schedule_rejected] - no row was stored. reason is StatifierOban.Timer.schedule_error/0, and the commonest value, {:non_self_target, target}, is the st-ADR-0055 bailout rather than a fault.

timer_scheduled(scope, effect, job)

@spec timer_scheduled(scope(), Statifier.Effect.SendDelayed.t(), Oban.Job.t()) :: :ok

Emits [:statifier_oban, :timer, :scheduled] - the durable write for one %Statifier.Effect.SendDelayed{} happened, and conflict? says whether it was new.