Current section

Files

Jump to
distribute src telemetry_ffi.erl
Raw

src/telemetry_ffi.erl

%% Telemetry sink storage. Single ETS slot, lock-free read on the hot
%% path, last-wins replacement on `install`. Default = absent slot ->
%% `emit/1` is a constant-time no-op.
%%
%% ETS over `persistent_term`: a `persistent_term:put/2` triggers a
%% global GC pass across every BEAM process. `install` is allowed to
%% be called more than once (tests swap sinks between cases) so we
%% cannot afford a global GC per install. ETS gives us O(1) read with
%% `read_concurrency`, lock-free on hot SMP, and `insert` without GC.
-module(telemetry_ffi).
-export([install/1, reset/0, emit/1]).
-define(TABLE, distribute_telemetry_sink_table).
-define(KEY, sink).
%% Lazy-init the ETS table. Race-safe: concurrent first callers
%% either win the `ets:new` or catch the `badarg` and discover the
%% existing table.
ensure_table() ->
case ets:whereis(?TABLE) of
undefined ->
try
ets:new(?TABLE, [
named_table, public, set, {read_concurrency, true}
])
catch
error:badarg -> ?TABLE
end;
_ ->
?TABLE
end.
install(Sink) when is_function(Sink, 1) ->
ensure_table(),
ets:insert(?TABLE, {?KEY, Sink}),
nil.
reset() ->
case ets:whereis(?TABLE) of
undefined -> nil;
_ ->
ets:delete(?TABLE, ?KEY),
nil
end.
%% Hot path: ETS lookup -> sink invocation. The sink is run inline
%% in the caller's process so a slow sink directly slows the emit
%% point. Library callers that want async fan-out should make the
%% sink itself send to a background process.
%%
%% Sink panics are caught and logged via `logger:warning/2` rather
%% than propagated. Rationale: `emit` is called from inside actor
%% mailbox handlers, the call selector, and the `call_isolated`
%% proxy. A buggy sink that raises would otherwise kill those
%% library-internal processes, taking down operationally critical
%% paths (registered actors, in-flight RPCs) for what is by
%% definition an observability bug. Catching keeps the library
%% resilient; logging keeps the bug visible to operators. We log
%% the event tag (not the full event payload) to avoid leaking
%% potentially sensitive `attempted_input` strings into shared logs.
emit(Event) ->
case ets:whereis(?TABLE) of
undefined ->
nil;
_ ->
case ets:lookup(?TABLE, ?KEY) of
[{?KEY, Sink}] ->
safe_invoke(Sink, Event),
nil;
[] ->
nil
end
end.
safe_invoke(Sink, Event) ->
try
Sink(Event)
catch
Class:Reason:Stack ->
EventTag = event_tag(Event),
logger:warning(
"distribute: telemetry sink raised on event ~p: ~p:~p~n~p",
[EventTag, Class, Reason, Stack]
),
nil
end.
%% Project an event onto its tag (the leading atom in the tuple form
%% Gleam constructors compile to). For unit constructors the value
%% is itself an atom, so we return it as-is.
event_tag(Event) when is_tuple(Event), tuple_size(Event) >= 1 ->
element(1, Event);
event_tag(Event) when is_atom(Event) ->
Event;
event_tag(_) ->
unknown_event_shape.