-module(pharos@alert_manager).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/pharos/alert_manager.gleam").
-export([from_name/1, start_link/3, breach/1, recover/1, sample/2]).
-export_type([alert_manager/0, message/0, data/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Per-threshold alert state machine.\n"
"\n"
" Wraps an `eparch/state_machine` running the four-state lifecycle\n"
" `Clear -> Pending -> Firing -> Cooling -> Clear`. \n"
" The state machine is registered under a deterministic `Name` so \n"
" that supervisor restarts.\n"
" keep `AlertManager` handles valid: callers send `Breach` / `Recover`\n"
" casts to the registered name, which the OTP runtime resolves to the\n"
" current pid on each call.\n"
"\n"
" `breach/1` and `recover/1` map physical signals (threshold crossed\n"
" up / down) to state transitions. The dispatcher is notified with\n"
" `AlertFiring` and `AlertResolved` events on the relevant transitions.\n"
).
-opaque alert_manager() :: {alert_manager, gleam@erlang@process:name(message())}.
-type message() :: breach | recover | {sample, float()}.
-type data() :: {data,
binary(),
pharos@alert:alert_level(),
integer(),
integer(),
pharos@event_bus:event_bus(),
gleam@option:option(pharos@alert:window_spec()),
list({integer(), float()})}.
-file("src/pharos/alert_manager.gleam", 58).
?DOC(
" Wrap a registered name as an `AlertManager`. The supervisor uses this\n"
" to build handles before the underlying state machine has been started,\n"
" so that the handler-attacher can route breach/recover signals to a\n"
" stable address.\n"
).
-spec from_name(gleam@erlang@process:name(message())) -> alert_manager().
from_name(Name) ->
{alert_manager, Name}.
-file("src/pharos/alert_manager.gleam", 233).
-spec fire(data()) -> nil.
fire(Data) ->
pharos@event_bus:notify(
erlang:element(6, Data),
{alert_firing,
erlang:element(2, Data),
erlang:element(3, Data),
pharos_ffi:collect_diagnostic()}
).
-file("src/pharos/alert_manager.gleam", 215).
-spec handle_state_timeout(pharos@alert:alert_state(), data()) -> eparch@state_machine:step(pharos@alert:alert_state(), data(), message(), any()).
handle_state_timeout(State, Data) ->
case State of
clear ->
eparch@state_machine:keep_state(Data, []);
pending ->
fire(Data),
eparch@state_machine:next_state(firing, Data, []);
firing ->
eparch@state_machine:keep_state(Data, []);
cooling ->
pharos@event_bus:notify(
erlang:element(6, Data),
{alert_resolved, erlang:element(2, Data)}
),
eparch@state_machine:next_state(clear, Data, [])
end.
-file("src/pharos/alert_manager.gleam", 169).
-spec handle_recover(pharos@alert:alert_state(), data()) -> eparch@state_machine:step(pharos@alert:alert_state(), data(), message(), any()).
handle_recover(State, Data) ->
case State of
clear ->
eparch@state_machine:keep_state(Data, []);
pending ->
eparch@state_machine:next_state(clear, Data, []);
firing ->
eparch@state_machine:next_state(
cooling,
Data,
[{state_timeout, {'after', erlang:element(5, Data)}, breach}]
);
cooling ->
eparch@state_machine:keep_state(Data, [])
end.
-file("src/pharos/alert_manager.gleam", 138).
-spec handle_breach(pharos@alert:alert_state(), data()) -> eparch@state_machine:step(pharos@alert:alert_state(), data(), message(), any()).
handle_breach(State, Data) ->
case State of
clear ->
case erlang:element(4, Data) of
0 ->
fire(Data),
eparch@state_machine:next_state(firing, Data, []);
Milliseconds ->
eparch@state_machine:next_state(
pending,
Data,
[{state_timeout, {'after', Milliseconds}, breach}]
)
end;
pending ->
eparch@state_machine:keep_state(Data, []);
firing ->
eparch@state_machine:keep_state(Data, []);
cooling ->
fire(Data),
eparch@state_machine:next_state(firing, Data, [])
end.
-file("src/pharos/alert_manager.gleam", 191).
?DOC(
" Fold a windowed sample into the bounded window, then drive the same\n"
" soak/cool transitions as a per-tick rule using the aggregate verdict.\n"
" Inert when no window is configured (a windowed signal to a per-tick rule).\n"
).
-spec handle_sample(pharos@alert:alert_state(), data(), float()) -> eparch@state_machine:step(pharos@alert:alert_state(), data(), message(), any()).
handle_sample(State, Data, Value) ->
case erlang:element(7, Data) of
none ->
eparch@state_machine:keep_state(Data, []);
{some, Spec} ->
Now = pharos_ffi:now_ms(),
Samples = pharos@alert:prune_samples(
[{Now, Value} | erlang:element(8, Data)],
Now,
erlang:element(2, Spec)
),
Data@1 = {data,
erlang:element(2, Data),
erlang:element(3, Data),
erlang:element(4, Data),
erlang:element(5, Data),
erlang:element(6, Data),
erlang:element(7, Data),
Samples},
case pharos@alert:window_breached(Samples, Now, Spec) of
true ->
handle_breach(State, Data@1);
false ->
handle_recover(State, Data@1)
end
end.
-file("src/pharos/alert_manager.gleam", 115).
-spec handle_event(
eparch@state_machine:event(pharos@alert:alert_state(), message(), HXU),
pharos@alert:alert_state(),
data()
) -> eparch@state_machine:step(pharos@alert:alert_state(), data(), message(), HXU).
handle_event(Event, State, Data) ->
case Event of
{cast, breach} ->
handle_breach(State, Data);
{cast, recover} ->
handle_recover(State, Data);
{cast, {sample, Value}} ->
handle_sample(State, Data, Value);
{timeout, state_timeout_type, _} ->
handle_state_timeout(State, Data);
{timeout, event_timeout_type, _} ->
eparch@state_machine:keep_state(Data, []);
{timeout, {generic_timeout_type, _}, _} ->
eparch@state_machine:keep_state(Data, []);
{info, _} ->
eparch@state_machine:keep_state(Data, []);
{call, _, _} ->
eparch@state_machine:keep_state(Data, []);
{enter, _} ->
eparch@state_machine:keep_state(Data, [])
end.
-file("src/pharos/alert_manager.gleam", 65).
?DOC(
" Start a state machine for `data`, registering it under `name`. Notifies\n"
" `bus` with `AlertFiring` / `AlertResolved` events on the relevant\n"
" transitions.\n"
).
-spec start_link(
pharos@alert:alert_data(),
pharos@event_bus:event_bus(),
gleam@erlang@process:name(message())
) -> {ok, alert_manager()} | {error, eparch@state_machine:start_error()}.
start_link(Data, Bus, Name) ->
{alert_data, Id, Level, Soak_period_ms, Cool_period_ms, Window} = Data,
Initial = {data, Id, Level, Soak_period_ms, Cool_period_ms, Bus, Window, []},
Result = begin
_pipe = eparch@state_machine:new(clear, Initial),
_pipe@1 = eparch@state_machine:named(_pipe, {local, Name}),
_pipe@2 = eparch@state_machine:on_event(_pipe@1, fun handle_event/3),
eparch@state_machine:start_link(_pipe@2)
end,
case Result of
{ok, _} ->
{ok, {alert_manager, Name}};
{error, Error} ->
{error, Error}
end.
-file("src/pharos/alert_manager.gleam", 111).
?DOC(" Resolve a manager's registered name to a `ServerRef` for casting.\n").
-spec server_ref(alert_manager()) -> eparch@state_machine:server_ref(message()).
server_ref(Manager) ->
statem_ffi:ref_from_subject(
gleam@erlang@process:named_subject(erlang:element(2, Manager))
).
-file("src/pharos/alert_manager.gleam", 95).
?DOC(" Signal that the threshold has been breached.\n").
-spec breach(alert_manager()) -> nil.
breach(Manager) ->
statem_ffi:cast(server_ref(Manager), breach).
-file("src/pharos/alert_manager.gleam", 100).
?DOC(" Signal that the threshold is no longer breached.\n").
-spec recover(alert_manager()) -> nil.
recover(Manager) ->
statem_ffi:cast(server_ref(Manager), recover).
-file("src/pharos/alert_manager.gleam", 106).
?DOC(
" Feed a sample to a windowed rule. The manager folds it into its window and\n"
" derives breach/recover from the aggregate.\n"
).
-spec sample(alert_manager(), float()) -> nil.
sample(Manager, Value) ->
statem_ffi:cast(server_ref(Manager), {sample, Value}).