Packages

A 'plugin-based' monitor and alert manager for BEAM Nodes and Hosts

Retired package: Deprecated

Current section

Files

Jump to
pharos src pharos_ffi.erl
Raw

src/pharos_ffi.erl

-module(pharos_ffi).
-export([
%% Telemetry poller wrapper
start_poller/1,
list_measurements/1,
%% Telemetry attach/detach
attach_many/4,
detach/1,
%% Memory unit conversion
convert_memory/2,
%% Alert diagnostic
collect_diagnostic/0,
%% Custom telemetry measurement emitters
emit_cluster_nodes/0,
emit_host_memory/0,
emit_host_disk/0,
%% Cross-typed-name plumbing for the event bus
manager_from_name/1,
%% Supervisor shutdown
shutdown_supervisor/1
]).
%% Politely stop a supervisor (and therefore its tree). Idempotent: a
%% second call against a dead pid is a no-op so `pharos:stop/1` is safe to
%% call multiple times.
shutdown_supervisor(Pid) when is_pid(Pid) ->
case erlang:is_process_alive(Pid) of
false ->
nil;
true ->
try
_ = supervisor:stop(Pid),
nil
catch
_:_ -> nil
end
end;
shutdown_supervisor(_Other) ->
nil.
%% ===========================================================================
%% Alert diagnostic
%% ===========================================================================
%% Capture a brief VM snapshot for the alert diagnostic field.
%% Returns a binary (Gleam String) describing memory usage and process count.
collect_diagnostic() ->
Memory = erlang:memory(),
TotalBytes = proplists:get_value(total, Memory, 0),
TotalMb = TotalBytes / 1_048_576,
Procs = erlang:system_info(process_count),
iolist_to_binary(
io_lib:format("memory=~.2fMb processes=~w", [TotalMb, Procs])
).
%% ===========================================================================
%% Telemetry poller
%% ===========================================================================
%% Translate Gleam-encoded poller options into the keyword list shape
%% telemetry_poller expects, then start the poller.
%%
%% Gleam's variant encoding (e.g. `{period, Ms}`, `{builtin, memory}`) does
%% not match telemetry_poller's expected term shapes (e.g. bare atom
%% `memory`, `{process_info, [{name, _}, ...]}`), so we translate at the
%% FFI boundary.
start_poller(GleamOptions) ->
ErlangOptions = lists:map(fun translate_option/1, GleamOptions),
case telemetry_poller:start_link(ErlangOptions) of
{ok, Pid} -> {ok, Pid};
ignore -> {error, poller_ignored};
{error, Reason} -> {error, {poller_start_failed, format_reason(Reason)}}
end.
translate_option({period, Ms}) ->
{period, Ms};
translate_option({measurements, Specs}) ->
{measurements, lists:map(fun translate_measurement/1, Specs)}.
translate_measurement({builtin, Name}) ->
%% Built-in measurements (`memory`, `total_run_queue_lengths`,
%% `system_counts`, `persistent_term`) are bare atoms in
%% telemetry_poller's API.
Name;
translate_measurement({process_info_spec, Name, Event, Keys}) ->
{process_info, [{name, Name}, {event, Event}, {keys, Keys}]};
translate_measurement({custom, Module, Function, Args}) ->
{Module, Function, Args}.
%% telemetry_poller normalises measurements into internal
%% {telemetry_poller_builtin, Name, []} tuples when storing them.
%% This shim converts those back to the bare atoms the Gleam side may want.
list_measurements(Poller) ->
lists:map(fun restore_measurement/1, telemetry_poller:list_measurements(Poller)).
restore_measurement({telemetry_poller_builtin, memory, _}) ->
memory;
restore_measurement({telemetry_poller_builtin, total_run_queue_lengths, _}) ->
total_run_queue_lengths;
restore_measurement({telemetry_poller_builtin, system_counts, _}) ->
system_counts;
restore_measurement({telemetry_poller_builtin, persistent_term, _}) ->
persistent_term;
restore_measurement(Other) ->
Other.
%% ===========================================================================
%% Telemetry attach / detach
%% ===========================================================================
%% Wraps a 2-arity Gleam handler in the 4-arity Erlang fun that
%% telemetry:attach_many/4 expects, packing the four arguments into a
%% TelemetryEvent record that Gleam can pattern-match on.
attach_many(HandlerId, Events, GleamHandler, Config) ->
Handler = fun(EventName, Measurements, Metadata, Cfg) ->
Event = {telemetry_event, EventName, Measurements, Metadata},
GleamHandler(Event, Cfg)
end,
case telemetry:attach_many(HandlerId, Events, Handler, Config) of
ok -> {ok, nil};
{error, already_exists} -> {error, handler_already_attached};
{error, Reason} -> {error, {attach_failed, format_reason(Reason)}}
end.
detach(HandlerId) ->
case telemetry:detach(HandlerId) of
ok -> {ok, nil};
{error, not_found} -> {error, handler_not_found};
{error, Reason} -> {error, {detach_failed, format_reason(Reason)}}
end.
%% ===========================================================================
%% Memory unit conversion
%% ===========================================================================
convert_memory(Measurements, Unit) ->
Factor =
case Unit of
kb -> 1024;
mb -> 1_048_576;
gb -> 1_073_741_824
end,
maps:map(fun(_Key, Bytes) -> Bytes / Factor end, Measurements).
%% ===========================================================================
%% Custom telemetry measurement emitters.
%%
%% Each function is invoked by telemetry_poller every polling cycle. They
%% emit telemetry events whose decoded shape lives in pharos/measurement.gleam.
%% ===========================================================================
%% Emit `[pharos, cluster, nodes]`: this node's name plus connected nodes.
emit_cluster_nodes() ->
Self = erlang:atom_to_binary(erlang:node(), utf8),
Nodes = [erlang:atom_to_binary(N, utf8) || N <- erlang:nodes()],
telemetry:execute(
[pharos, cluster, nodes],
#{count => length(Nodes)},
#{self => Self, nodes => Nodes}
).
%% Scaffolded host-memory probe. Emits a placeholder event with
%% `status: unimplemented` so subscribers can verify wiring before the real
%% probe lands.
emit_host_memory() ->
telemetry:execute(
[pharos, host, memory],
#{total_bytes => 0, used_bytes => 0, available_bytes => 0},
#{status => unimplemented}
).
%% Scaffolded host-disk probe. Same shape as `emit_host_memory/0`.
emit_host_disk() ->
telemetry:execute(
[pharos, host, disk],
#{total_bytes => 0, used_bytes => 0, available_bytes => 0},
#{status => unimplemented}
).
%% ===========================================================================
%% Name-to-Manager cast.
%%
%% At the Erlang level a `Name(AlertEvent)` is just a registered atom and an
%% eparch `Manager(AlertEvent)` is just a pid-or-name reference. gen_event
%% accepts atoms wherever it accepts pids, so this is a zero-cost identity
%% function that crosses the Gleam type boundary.
%% ===========================================================================
manager_from_name(Name) -> Name.
%% ===========================================================================
%% Internal
%% ===========================================================================
format_reason(Reason) ->
iolist_to_binary(io_lib:format("~p", [Reason])).