%%%------------------------------------------------------------------- %%% @doc Plugin metrics facade. %%% %%% Lightweight metrics using OTP counters (atomics-backed, lock-free) %%% with an ETS registry. Each plugin gets its own counters reference %%% with up to 64 metric slots. %%% %%% Two metric types: %%% counter — monotonically increasing (e.g. requests served) %%% gauge — point-in-time value (e.g. last latency) %%% %%% Plugins use macros from hecate_plugin.hrl: %%% ?METRIC_INC(Name) — increment counter by 1 %%% ?METRIC_ADD(Name, N) — increment counter by N %%% ?METRIC_SET(Name, Val) — set gauge to Val %%% @end %%%------------------------------------------------------------------- -module(hecate_plugin_metrics). -export([init/1, counter/3, gauge/3, get_all/0, get_plugin/1, cleanup/1]). -define(TABLE, hecate_plugin_metrics). -define(SLOTS_PER_PLUGIN, 64). %%-------------------------------------------------------------------- %% @doc Initialize metrics namespace for a plugin. %% Creates a counters reference with 64 slots. %% Safe to call multiple times (idempotent). %% @end %%-------------------------------------------------------------------- -spec init(binary()) -> ok. init(PluginName) -> ensure_table(), case ets:lookup(?TABLE, {PluginName, <<"__counters_ref__">>}) of [_] -> ok; [] -> Ref = counters:new(?SLOTS_PER_PLUGIN, [write_concurrency]), ets:insert(?TABLE, {{PluginName, <<"__counters_ref__">>}, Ref, 0, ref}), ok end. %%-------------------------------------------------------------------- %% @doc Increment a counter metric by N. %% @end %%-------------------------------------------------------------------- -spec counter(binary(), binary(), integer()) -> ok. counter(PluginName, MetricName, N) -> {Ref, Slot} = get_or_create_slot(PluginName, MetricName, counter), counters:add(Ref, Slot, N), ok. %%-------------------------------------------------------------------- %% @doc Set a gauge metric to Value. %% @end %%-------------------------------------------------------------------- -spec gauge(binary(), binary(), integer()) -> ok. gauge(PluginName, MetricName, Value) -> {Ref, Slot} = get_or_create_slot(PluginName, MetricName, gauge), counters:put(Ref, Slot, Value), ok. %%-------------------------------------------------------------------- %% @doc Get all metrics for all plugins. %% Returns a list of maps with plugin, name, type, and value keys. %% @end %%-------------------------------------------------------------------- -spec get_all() -> [map()]. get_all() -> case ets:info(?TABLE) of undefined -> []; _ -> ets:foldl(fun ({{_Plugin, <<"__counters_ref__">>}, _Ref, _Slot, ref}, Acc) -> Acc; ({{Plugin, Name}, _Ref, Slot, Type}, Acc) -> case get_counter_ref(Plugin) of undefined -> Acc; CRef -> Value = counters:get(CRef, Slot), [#{plugin => Plugin, name => Name, type => Type, value => Value} | Acc] end end, [], ?TABLE) end. %%-------------------------------------------------------------------- %% @doc Get all metrics for a specific plugin. %% @end %%-------------------------------------------------------------------- -spec get_plugin(binary()) -> [map()]. get_plugin(PluginName) -> case ets:info(?TABLE) of undefined -> []; _ -> case get_counter_ref(PluginName) of undefined -> []; CRef -> ets:foldl(fun ({{P, <<"__counters_ref__">>}, _, _, ref}, Acc) when P =:= PluginName -> Acc; ({{P, Name}, _, Slot, Type}, Acc) when P =:= PluginName -> Value = counters:get(CRef, Slot), [#{plugin => PluginName, name => Name, type => Type, value => Value} | Acc]; (_, Acc) -> Acc end, [], ?TABLE) end end. %%-------------------------------------------------------------------- %% @doc Remove all metrics for a plugin. %% @end %%-------------------------------------------------------------------- -spec cleanup(binary()) -> ok. cleanup(PluginName) -> case ets:info(?TABLE) of undefined -> ok; _ -> %% Collect keys to delete, then delete them Keys = ets:foldl(fun ({{P, Name}, _, _, _}, Acc) when P =:= PluginName -> [{P, Name} | Acc]; (_, Acc) -> Acc end, [], ?TABLE), lists:foreach(fun(Key) -> ets:delete(?TABLE, Key) end, Keys), ok end. %%==================================================================== %% Internal %%==================================================================== ensure_table() -> case ets:info(?TABLE) of undefined -> ets:new(?TABLE, [set, named_table, public, {read_concurrency, true}, {write_concurrency, true}]); _ -> ok end. get_counter_ref(PluginName) -> case ets:lookup(?TABLE, {PluginName, <<"__counters_ref__">>}) of [{{_, _}, Ref, _, ref}] -> Ref; [] -> undefined end. get_or_create_slot(PluginName, MetricName, Type) -> ensure_table(), Key = {PluginName, MetricName}, case ets:lookup(?TABLE, Key) of [{_, _Ref, Slot, _Type}] -> {get_counter_ref(PluginName), Slot}; [] -> Ref = ensure_counter_ref(PluginName), Slot = next_slot(PluginName), ets:insert(?TABLE, {Key, Ref, Slot, Type}), {Ref, Slot} end. ensure_counter_ref(PluginName) -> case get_counter_ref(PluginName) of undefined -> init(PluginName), get_counter_ref(PluginName); Ref -> Ref end. next_slot(PluginName) -> MaxSlot = ets:foldl(fun ({{P, <<"__counters_ref__">>}, _, _, ref}, Max) when P =:= PluginName -> Max; ({{P, _Name}, _, Slot, _Type}, Max) when P =:= PluginName -> max(Slot, Max); (_, Max) -> Max end, 0, ?TABLE), MaxSlot + 1.