asobi_telemetry (asobi v0.84.0)
View SourceSummary
Functions
Every event name this module emits - the surface locked by ADR 0005
(docs/adr/0005-telemetry-event-surface.md).
Emit [asobi, error] with no extra context. See game_error/2.
Emit [asobi, error] for a game-code error - the game author's logic failing,
e.g. a Lua callback raising. count => 1, metadata #{kind, details}.
asobi#193: a player hit the per-identity join rate cap.
asobi#248: a player hit the per-identity or global zone-crossing rate cap.
Fires per denied crossing - the denied entity is clamped back inside its
current zone (private, see asobi_zone's clamp_to_zone/3), but under
sustained input the crossing is still re-detected (and re-denied) every
tick, so a client thrashing a boundary can drive this at up to the world
tick rate. Aggregate this; do not use player_id as a metric label.
asobi#313: how long a world tick took, sampled.
#160: a WS upgrade was rejected by the Origin allowlist.
asobi#313: a zone process went away (reaped, crashed, or world shutdown). See zone_opened/2.
asobi#313: a zone process started. Zones are lazy, so live-zone count is not
derivable from world count; pair this with zone_closed/2 and track the
difference as a gauge. Both metadata keys are unbounded - never a label.
asobi#426: count zones were skipped by this world tick because they had not
yet retired the previous one.
Types
Functions
-spec auth_cache_hit(positive | negative) -> ok.
-spec auth_cache_miss(positive | negative) -> ok.
-spec auth_cache_sweep() -> ok.
-spec events() -> [telemetry:event_name()].
Every event name this module emits - the surface locked by ADR 0005
(docs/adr/0005-telemetry-event-surface.md).
Exported so a consumer attaches to the whole surface without restating it.
Restating it is what let the built-in debug logger and opentelemetry_asobi
both drift to the same stale 24-name subset, leaving the failure and abuse
signals (rate limits, anticheat, [asobi, error]) invisible in dev (#312).
asobi_telemetry_tests asserts this list against the names actually passed
to telemetry:execute/3 in this module, so the two cannot drift again.
-spec game_error(game_error_kind()) -> ok.
Emit [asobi, error] with no extra context. See game_error/2.
-spec game_error(game_error_kind(), map()) -> ok.
Emit [asobi, error] for a game-code error - the game author's logic failing,
e.g. a Lua callback raising. count => 1, metadata #{kind, details}.
details is passed verbatim to every attached handler (which may log or export
it), so emitters MUST keep it bounded and free of sensitive data: no raw player
input, secrets, PII, or file-system paths, and no unbounded values (truncate a
message, classify the reason - never pass a raw luerl/error term, which can
embed interpolated player input). Consumers should aggregate on kind only, not
on details (this event can fire at high frequency).
-spec handle_event(telemetry:event_name(), telemetry:event_measurements(), telemetry:event_metadata(), telemetry:handler_config()) -> ok.
-spec join_rate_limited(binary()) -> ok.
asobi#193: a player hit the per-identity join rate cap.
-spec match_finished(binary(), pos_integer(), map()) -> ok.
-spec matchmaker_failed(binary(), non_neg_integer()) -> ok.
-spec matchmaker_formed(binary(), pos_integer(), pos_integer()) -> ok.
-spec rehome_rate_limited(binary()) -> ok.
asobi#248: a player hit the per-identity or global zone-crossing rate cap.
Fires per denied crossing - the denied entity is clamped back inside its
current zone (private, see asobi_zone's clamp_to_zone/3), but under
sustained input the crossing is still re-detected (and re-denied) every
tick, so a client thrashing a boundary can drive this at up to the world
tick rate. Aggregate this; do not use player_id as a metric label.
-spec session_connected(binary()) -> ok.
-spec session_disconnected(binary(), pos_integer()) -> ok.
-spec setup() -> ok.
-spec vote_resolved(binary(), pos_integer(), map()) -> ok.
-spec world_finished(binary(), pos_integer(), map()) -> ok.
-spec world_tick(binary() | undefined, non_neg_integer(), non_neg_integer(), non_neg_integer()) -> ok.
asobi#313: how long a world tick took, sampled.
A world tick is the fan-out to every zone plus the fan-in of their
tick_done replies, so this is the saturation signal that degrades first
under entity load. Emitting it at the world tick rate (20 Hz by default) is
too hot for a raw sink, so asobi_world_ticker samples roughly once a second
and carries max_duration_ms - the worst tick in the sampled window -
alongside the sampled tick's own duration_ms. Alert on the max; a sampled
duration alone hides exactly the spikes worth paging on.
zone_count is how many zones that tick fanned out to. world_id is
unbounded (one per live world) - never a metric label.
-spec ws_connect_rate_limited(binary()) -> ok.
-spec ws_connected() -> ok.
-spec ws_disconnected() -> ok.
-spec ws_idle_auth_timeout() -> ok.
-spec ws_message_in(binary()) -> ok.
-spec ws_message_out(binary()) -> ok.
-spec ws_origin_rejected() -> ok.
#160: a WS upgrade was rejected by the Origin allowlist.
asobi#313: a zone process went away (reaped, crashed, or world shutdown). See zone_opened/2.
asobi#313: a zone process started. Zones are lazy, so live-zone count is not
derivable from world count; pair this with zone_closed/2 and track the
difference as a gauge. Both metadata keys are unbounded - never a label.
-spec zone_tick_skipped(binary() | undefined, pos_integer()) -> ok.
asobi#426: count zones were skipped by this world tick because they had not
yet retired the previous one.
This is the back-pressure signal. A steady trickle is a world running close to
its tick budget; a count that climbs toward the world's zone count and stays
there is a world that can no longer keep up, and before #426 the only symptom
of that was CPU. Unlike world_tick/4 this is not sampled - it is emitted
only on a tick that actually skipped, so a healthy world emits nothing at all.