asobi_telemetry (asobi v0.84.0)

View Source

Summary

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

game_error_kind()

-type game_error_kind() :: lua_error | unknown_spawn_template | zone_unavailable.

Functions

anticheat_violation(PlayerId, Type, Details)

-spec anticheat_violation(binary(), atom(), map()) -> ok.

auth_cache_hit(Kind)

-spec auth_cache_hit(positive | negative) -> ok.

auth_cache_miss(Kind)

-spec auth_cache_miss(positive | negative) -> ok.

auth_cache_sweep()

-spec auth_cache_sweep() -> ok.

chat_message_sent(ChannelId, SenderId)

-spec chat_message_sent(binary(), binary()) -> ok.

economy_transaction(PlayerId, Currency, Amount, Reason)

-spec economy_transaction(binary(), binary(), integer(), binary()) -> ok.

events()

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

game_error(Kind)

-spec game_error(game_error_kind()) -> ok.

Emit [asobi, error] with no extra context. See game_error/2.

game_error(Kind, Details)

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

handle_event(EventName, Measurements, Metadata, Config)

join_rate_limited(PlayerId)

-spec join_rate_limited(binary()) -> ok.

asobi#193: a player hit the per-identity join rate cap.

match_finished(MatchId, DurationMs, Result)

-spec match_finished(binary(), pos_integer(), map()) -> ok.

match_player_joined(MatchId, PlayerId)

-spec match_player_joined(binary(), binary()) -> ok.

match_player_left(MatchId, PlayerId)

-spec match_player_left(binary(), binary()) -> ok.

match_started(MatchId, Mode)

-spec match_started(binary(), binary() | undefined) -> ok.

matchmaker_deduped(PlayerId, Mode)

-spec matchmaker_deduped(binary(), binary()) -> ok.

matchmaker_failed(Mode, PlayerCount)

-spec matchmaker_failed(binary(), non_neg_integer()) -> ok.

matchmaker_formed(Mode, PlayerCount, WaitMs)

-spec matchmaker_formed(binary(), pos_integer(), pos_integer()) -> ok.

matchmaker_queued(PlayerId, Mode)

-spec matchmaker_queued(binary(), binary() | undefined) -> ok.

matchmaker_removed(PlayerId, Reason)

-spec matchmaker_removed(binary(), atom()) -> ok.

rehome_rate_limited(PlayerId)

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

session_connected(PlayerId)

-spec session_connected(binary()) -> ok.

session_disconnected(PlayerId, DurationMs)

-spec session_disconnected(binary(), pos_integer()) -> ok.

setup()

-spec setup() -> ok.

store_purchase(PlayerId, ItemId, Cost)

-spec store_purchase(binary(), binary(), integer()) -> ok.

vote_cast(VoteId, PlayerId)

-spec vote_cast(binary(), binary()) -> ok.

vote_resolved(VoteId, DurationMs, Result)

-spec vote_resolved(binary(), pos_integer(), map()) -> ok.

vote_started(VoteId, Method)

-spec vote_started(binary(), binary()) -> ok.

world_finished(WorldId, DurationMs, Result)

-spec world_finished(binary(), pos_integer(), map()) -> ok.

world_phase_changed(WorldId, FromPhase, ToPhase)

-spec world_phase_changed(binary(), binary(), binary()) -> ok.

world_player_joined(WorldId, PlayerId)

-spec world_player_joined(binary(), binary()) -> ok.

world_player_left(WorldId, PlayerId)

-spec world_player_left(binary(), binary()) -> ok.

world_started(WorldId, Mode)

-spec world_started(binary(), binary() | undefined) -> ok.

world_tick(WorldId, DurationMs, MaxDurationMs, ZoneCount)

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

ws_connect_rate_limited(PeerIp)

-spec ws_connect_rate_limited(binary()) -> ok.

ws_connected()

-spec ws_connected() -> ok.

ws_disconnected()

-spec ws_disconnected() -> ok.

ws_idle_auth_timeout()

-spec ws_idle_auth_timeout() -> ok.

ws_message_in(Type)

-spec ws_message_in(binary()) -> ok.

ws_message_out(Type)

-spec ws_message_out(binary()) -> ok.

ws_origin_rejected()

-spec ws_origin_rejected() -> ok.

#160: a WS upgrade was rejected by the Origin allowlist.

zone_closed(WorldId, Coords)

-spec zone_closed(binary() | undefined, {integer(), integer()}) -> ok.

asobi#313: a zone process went away (reaped, crashed, or world shutdown). See zone_opened/2.

zone_opened(WorldId, Coords)

-spec zone_opened(binary() | undefined, {integer(), integer()}) -> ok.

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.

zone_tick_skipped(WorldId, Count)

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