Packages
reckon_db
5.10.0
5.11.0
5.10.4
5.10.3
5.10.1
5.10.0
5.9.1
5.9.0
5.8.3
5.8.2
5.8.1
5.8.0
5.7.0
5.6.1
5.6.0
5.5.5
5.5.4
5.5.3
5.5.2
5.5.1
5.5.0
5.4.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.0.0
3.1.2
3.1.1
3.0.0
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.2
2.2.0
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
BEAM-native Event Store built on Khepri/Ra with Raft consensus. Event sourcing, persistent subscriptions, snapshots, and automatic cluster formation via UDP multicast discovery. Ships embedded Rust NIFs for 3-15x acceleration of crypto, hashing, compression, aggregation, filter matching, and grap...
Current section
Files
Jump to
Current section
Files
src/reckon_db_resource_monitor.erl
%% @doc System resource sampling for reckon-db (CPU + disk).
%%
%% A node-wide singleton gen_server that samples host CPU and disk usage on a
%% fixed interval and emits telemetry gauges — unlike `reckon_db_memory', which
%% fires only when a pressure LEVEL changes, this emits a fresh sample every
%% tick so consumers can graph a live series.
%%
%% Telemetry (see reckon_db_telemetry.hrl):
%% <ul>
%% <li>`[reckon_db, cpu, sample]' — measurements `busy_percent', `load1',
%% `load5', `load15'; metadata `cores'.</li>
%% <li>`[reckon_db, disk, sample]' — one per mount; measurements
%% `used_percent', `total_kb', `available_kb'; metadata `mount',
%% `data_dir_mount'.</li>
%% </ul>
%%
%% CPU/disk come from `cpu_sup'/`disksup' (os_mon). The monitor tries to start
%% os_mon on init and DEGRADES GRACEFULLY if it can't: `get_stats/0' reports
%% `os_mon => false' and no CPU/disk telemetry is emitted (there is no
%% pure-BEAM fallback for host CPU% or disk usage). Started from
%% `reckon_db_sup' when `resource_monitoring' is enabled (the default).
%%
%% @author rgfaber
-module(reckon_db_resource_monitor).
-behaviour(gen_server).
-include("reckon_db.hrl").
-include("reckon_db_telemetry.hrl").
-export([start_link/0, start_link/1, get_stats/0, sample_now/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(DEFAULT_INTERVAL, 10000). %% 10s
-record(state, {
interval :: pos_integer(),
data_dir :: string() | undefined,
os_mon :: boolean(),
timer :: reference() | undefined,
cpu :: map() | undefined,
disk = [] :: [map()],
last_sample :: integer() | undefined
}).
%%====================================================================
%% API
%%====================================================================
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() -> start_link(#{}).
%% @doc Start the monitor. Config keys: `interval' (ms, default 10000),
%% `data_dir' (string; the store data root, to flag its mount — resolved from
%% the configured stores when omitted).
-spec start_link(map()) -> {ok, pid()} | {error, term()}.
start_link(Config) ->
gen_server:start_link({local, ?SERVER}, ?MODULE, Config, []).
%% @doc Latest sampled resource snapshot:
%% `#{os_mon => boolean(), cpu => map()|undefined, disk => [map()],
%% last_sample => ms}'.
-spec get_stats() -> map().
get_stats() -> gen_server:call(?SERVER, get_stats).
%% @doc Force an immediate sample and return the fresh snapshot.
-spec sample_now() -> map().
sample_now() -> gen_server:call(?SERVER, sample_now).
%%====================================================================
%% gen_server
%%====================================================================
init(Config) ->
Interval = maps:get(interval, Config, ?DEFAULT_INTERVAL),
OsMon = ensure_os_mon(),
DataDir = resolve_data_dir(Config),
%% Prime cpu_sup: its first util/0 reading is the since-boot baseline;
%% discard it so the first real sample reflects the live interval.
_ = (catch cpu_sup:util()),
S0 = #state{interval = Interval, data_dir = DataDir, os_mon = OsMon},
S1 = do_sample(S0),
{ok, S1#state{timer = schedule(Interval)}}.
handle_call(get_stats, _From, State) ->
{reply, stats_map(State), State};
handle_call(sample_now, _From, State) ->
S1 = do_sample(State),
{reply, stats_map(S1), S1};
handle_call(_Req, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info(sample, State) ->
S1 = do_sample(State),
{noreply, S1#state{timer = schedule(S1#state.interval)}};
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, #state{timer = Timer}) ->
cancel(Timer),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%====================================================================
%% Internal
%%====================================================================
-spec ensure_os_mon() -> boolean().
ensure_os_mon() ->
case application:ensure_all_started(os_mon) of
{ok, _} -> true;
_Error -> false
end.
resolve_data_dir(Config) ->
case maps:get(data_dir, Config, undefined) of
undefined -> data_dir_from_stores();
Dir -> Dir
end.
%% @private Best-effort: the first configured store's data_dir, so we can flag
%% which mount actually holds event data.
data_dir_from_stores() ->
try reckon_db_config:get_all_store_configs() of
[#store_config{data_dir = D} | _] when is_list(D), D =/= [] -> D;
_ -> undefined
catch _:_ -> undefined end.
-spec do_sample(#state{}) -> #state{}.
do_sample(#state{data_dir = DataDir} = State) ->
Cpu = sample_cpu(),
Disk = sample_disk(DataDir),
emit_cpu(Cpu),
emit_disk(Disk),
State#state{cpu = Cpu, disk = Disk, last_sample = now_ms()}.
%% --- CPU ---
sample_cpu() ->
Busy = case (catch cpu_sup:util()) of
U when is_number(U) -> round_2(U);
_ -> undefined
end,
#{busy_percent => Busy,
load1 => load_avg(fun cpu_sup:avg1/0),
load5 => load_avg(fun cpu_sup:avg5/0),
load15 => load_avg(fun cpu_sup:avg15/0),
cores => cores()}.
%% cpu_sup:avgN/0 returns the load average * 256 (integer); normalise to a float.
load_avg(F) ->
case (catch F()) of
N when is_integer(N) -> round_2(N / 256);
_ -> undefined
end.
cores() ->
case erlang:system_info(logical_processors_available) of
N when is_integer(N) -> N;
_ -> erlang:system_info(schedulers_online)
end.
emit_cpu(#{busy_percent := undefined}) ->
ok; %% os_mon/cpu_sup unavailable — emit nothing
emit_cpu(#{busy_percent := Busy, load1 := L1, load5 := L5,
load15 := L15, cores := Cores}) ->
telemetry:execute(?CPU_SAMPLE,
#{busy_percent => Busy, load1 => num(L1),
load5 => num(L5), load15 => num(L15)},
#{cores => Cores}).
%% --- Disk ---
sample_disk(DataDir) ->
case (catch disksup:get_disk_data()) of
Data when is_list(Data), Data =/= [] ->
Mount = data_dir_mount(DataDir, Data),
[disk_entry(Id, TotalKb, UsedPct, Id =:= Mount)
|| {Id, TotalKb, UsedPct} <- Data, is_integer(TotalKb)];
_ -> []
end.
disk_entry(Id, TotalKb, UsedPct, IsDataDir) ->
Avail = round(TotalKb * (100 - UsedPct) / 100),
#{mount => unicode:characters_to_binary(Id),
total_kb => TotalKb, used_percent => UsedPct,
available_kb => Avail, data_dir_mount => IsDataDir}.
%% The store's mount = the longest disksup mount id that prefixes the data dir.
data_dir_mount(undefined, _Data) -> undefined;
data_dir_mount(DataDir, Data) ->
Prefixes = [Id || {Id, _, _} <- Data, lists:prefix(Id, DataDir)],
case lists:sort(fun(A, B) -> length(A) >= length(B) end, Prefixes) of
[Best | _] -> Best;
[] -> undefined
end.
emit_disk(Entries) ->
lists:foreach(
fun(#{mount := M, total_kb := T, used_percent := U,
available_kb := A, data_dir_mount := DDM}) ->
telemetry:execute(?DISK_SAMPLE,
#{used_percent => U, total_kb => T, available_kb => A},
#{mount => M, data_dir_mount => DDM})
end, Entries).
%% --- helpers ---
stats_map(#state{os_mon = OsMon, cpu = Cpu, disk = Disk, last_sample = LS}) ->
#{os_mon => OsMon, cpu => Cpu, disk => Disk, last_sample => LS}.
num(undefined) -> 0;
num(N) -> N.
round_2(F) -> round(F * 100) / 100.
now_ms() -> erlang:system_time(millisecond).
schedule(Interval) -> erlang:send_after(Interval, self(), sample).
cancel(undefined) -> ok;
cancel(Ref) -> erlang:cancel_timer(Ref), ok.