This commit is contained in:
@@ -131,7 +131,7 @@ create_admin(Req) ->
|
||||
case logic_admin:create_admin(Email, Password, Role) of
|
||||
{ok, Admin} ->
|
||||
% Аудит создания администратора
|
||||
admin_utils:log_admin_action(SuperAdminId, <<"create_admin">>, <<"admin">>, Email, Req2),
|
||||
admin_utils:log_admin_action(SuperAdminId, <<"create_admin">>, <<"admin">>, Email, <<>>,Req2),
|
||||
handler_utils:send_json(Req2, 201, handler_utils:admin_to_json(Admin));
|
||||
{error, email_exists} ->
|
||||
handler_utils:send_error(Req2, 409, <<"Email already exists">>);
|
||||
|
||||
@@ -134,7 +134,7 @@ update_admin(Req) ->
|
||||
Updates = convert_admin_fields(Updates0),
|
||||
case logic_admin:update_admin(Id, Updates) of
|
||||
{ok, Admin} ->
|
||||
admin_utils:log_admin_action(SuperAdminId, <<"update_admin">>, <<"admin">>, Id, Req2),
|
||||
admin_utils:log_admin_action(SuperAdminId, <<"update_admin">>, <<"admin">>, Id, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:admin_to_json(Admin));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Admin not found">>);
|
||||
@@ -158,7 +158,7 @@ delete_admin(Req) ->
|
||||
Id = cowboy_req:binding(id, Req1),
|
||||
case logic_admin:delete_admin(Id) of
|
||||
{ok, _} ->
|
||||
admin_utils:log_admin_action(SuperAdminId, <<"delete_admin">>, <<"admin">>, Id, Req1),
|
||||
admin_utils:log_admin_action(SuperAdminId, <<"delete_admin">>, <<"admin">>, Id, <<>>,Req1),
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Admin not found">>);
|
||||
|
||||
@@ -180,7 +180,7 @@ add_word(Req) ->
|
||||
#{<<"word">> := Word} ->
|
||||
case core_banned_words:add_banned_word(Word, AdminId) of
|
||||
{ok, _} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"add">>, <<"banned_word">>, Word, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"add">>, <<"banned_word">>, Word, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 201, #{status => <<"added">>});
|
||||
{error, already_exists} ->
|
||||
handler_utils:send_error(Req2, 409, <<"Word already exists">>);
|
||||
@@ -205,7 +205,7 @@ update_word(Req) ->
|
||||
case core_banned_words:update_banned_word(OldWord, NewWord) of
|
||||
{ok, _Updated} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"update">>, <<"banned_word">>,
|
||||
#{old_word => OldWord, new_word => NewWord}, Req2),
|
||||
#{old_word => OldWord, new_word => NewWord}, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, #{status => <<"updated">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Word not found">>);
|
||||
@@ -228,7 +228,7 @@ delete_word(Req) ->
|
||||
Word = cowboy_req:binding(word, Req1),
|
||||
case core_banned_words:remove_banned_word(Word) of
|
||||
{ok, _} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"delete">>, <<"banned_word">>, Word, Req1),
|
||||
admin_utils:log_admin_action(AdminId, <<"delete">>, <<"banned_word">>, Word, <<>>, Req1),
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Word not found">>);
|
||||
@@ -260,7 +260,7 @@ batch_add_words(Req) ->
|
||||
Added = length([R || R <- Results, maps:get(status, R) =:= <<"added">>]),
|
||||
Skipped = length([R || R <- Results, maps:get(status, R) =:= <<"skipped">>]),
|
||||
admin_utils:log_admin_action(AdminId, <<"batch_add">>, <<"banned_words">>,
|
||||
#{count => length(Words), added => Added, skipped => Skipped}, Req2),
|
||||
#{count => length(Words), added => Added, skipped => Skipped}, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, #{
|
||||
results => Results,
|
||||
added => Added,
|
||||
|
||||
@@ -114,7 +114,7 @@ update_calendar(Req) ->
|
||||
Updates ->
|
||||
case logic_calendar:admin_update(Id, Updates) of
|
||||
{ok, Updated} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"update">>, <<"calendar">>, Id, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update">>, <<"calendar">>, Id, <<>>, Req2),
|
||||
Json = handler_utils:calendar_to_json(Updated),
|
||||
handler_utils:send_json(Req2, 200, Json);
|
||||
{error, not_found} ->
|
||||
@@ -139,7 +139,7 @@ delete_calendar(Req) ->
|
||||
Id = cowboy_req:binding(id, Req1),
|
||||
case logic_calendar:admin_delete(Id) of
|
||||
ok ->
|
||||
admin_utils:log_admin_action(AdminId, <<"delete">>, <<"calendar">>, Id, Req1),
|
||||
admin_utils:log_admin_action(AdminId, <<"delete">>, <<"calendar">>, Id, <<>>, Req1),
|
||||
handler_utils:send_json(Req1, 200, #{});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Calendar not found">>)
|
||||
|
||||
@@ -154,7 +154,7 @@ update_event(Req) ->
|
||||
case logic_event:update_event_admin(EventId, UpdatesWithTypes) of
|
||||
{ok, Event} ->
|
||||
% Аудит обновления события
|
||||
admin_utils:log_admin_action(AdminId, <<"update_event">>, <<"event">>, EventId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_event">>, <<"event">>, EventId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:event_to_json(Event));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Event not found">>);
|
||||
@@ -177,7 +177,7 @@ delete_event(Req) ->
|
||||
case logic_event:delete_event_admin(EventId) of
|
||||
{ok, _} ->
|
||||
% Аудит удаления события
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_event">>, <<"event">>, EventId, Req1),
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_event">>, <<"event">>, EventId, <<>>, Req1),
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Event not found">>);
|
||||
@@ -218,9 +218,12 @@ convert_field({<<"tags">>, Val}) -> {tags, Val};
|
||||
convert_field({<<"capacity">>, Val}) -> {capacity, Val};
|
||||
convert_field({<<"online_link">>, Val}) -> {online_link, Val};
|
||||
convert_field({<<"status">>, Val}) ->
|
||||
try binary_to_existing_atom(Val, utf8) of
|
||||
Atom -> {status, Atom}
|
||||
catch
|
||||
error:badarg -> {status, Val}
|
||||
case Val of
|
||||
<<"active">> -> {status, active};
|
||||
<<"cancelled">> -> {status, cancelled};
|
||||
<<"completed">> -> {status, completed};
|
||||
<<"frozen">> -> {status, frozen};
|
||||
<<"deleted">> -> {status, deleted};
|
||||
_ -> {status, Val}
|
||||
end;
|
||||
convert_field(Other) -> Other.
|
||||
@@ -25,7 +25,7 @@ init(Req0, _State) ->
|
||||
core_admin_session:create(UserId, RefreshToken),
|
||||
core_admin:update_last_login(UserId),
|
||||
% Аудит успешного входа
|
||||
admin_utils:log_admin_action(UserId, <<"login">>, <<"admin">>, Email, Req1),
|
||||
admin_utils:log_admin_action(UserId, <<"login">>, <<"admin">>, Email, <<>>, Req1),
|
||||
Resp = #{
|
||||
<<"token">> => Token,
|
||||
<<"user">> => #{
|
||||
|
||||
@@ -116,7 +116,7 @@ update_me(Req) ->
|
||||
case logic_admin:update_admin(AdminId, Updates) of
|
||||
{ok, Admin} ->
|
||||
% Аудит обновления профиля
|
||||
admin_utils:log_admin_action(AdminId, <<"update_me">>, <<"admin">>, AdminId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_me">>, <<"admin">>, AdminId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:admin_to_json(Admin));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Admin not found">>);
|
||||
|
||||
@@ -101,7 +101,7 @@ apply_moderation(<<"user">>, Id, Action, Reason, Req, AdminId) ->
|
||||
handle_calendar(Id, <<"freeze">>, Reason, Req, AdminId) ->
|
||||
case core_calendar:freeze(Id, Reason) of
|
||||
{ok, Calendar} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"freeze_calendar">>, <<"calendar">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"freeze_calendar">>, <<"calendar">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:calendar_to_json(Calendar));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"Calendar not found">>)
|
||||
@@ -109,7 +109,7 @@ handle_calendar(Id, <<"freeze">>, Reason, Req, AdminId) ->
|
||||
handle_calendar(Id, <<"unfreeze">>, Reason, Req, AdminId) ->
|
||||
case core_calendar:unfreeze(Id, Reason) of
|
||||
{ok, Calendar} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"unfreeze_calendar">>, <<"calendar">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"unfreeze_calendar">>, <<"calendar">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:calendar_to_json(Calendar));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"Calendar not found">>)
|
||||
@@ -120,7 +120,7 @@ handle_calendar(_Id, _Action, _Reason, Req, _AdminId) ->
|
||||
handle_event(Id, <<"freeze">>, Reason, Req, AdminId) ->
|
||||
case core_event:freeze(Id, Reason) of
|
||||
{ok, Event} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"freeze_event">>, <<"event">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"freeze_event">>, <<"event">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:event_to_json(Event));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"Event not found">>)
|
||||
@@ -128,7 +128,7 @@ handle_event(Id, <<"freeze">>, Reason, Req, AdminId) ->
|
||||
handle_event(Id, <<"unfreeze">>, Reason, Req, AdminId) ->
|
||||
case core_event:unfreeze(Id, Reason) of
|
||||
{ok, Event} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"unfreeze_event">>, <<"event">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"unfreeze_event">>, <<"event">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:event_to_json(Event));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"Event not found">>)
|
||||
@@ -139,7 +139,7 @@ handle_event(_Id, _Action, _Reason, Req, _AdminId) ->
|
||||
handle_review(Id, <<"hide">>, Reason, Req, AdminId) ->
|
||||
case core_review:hide(Id, Reason) of
|
||||
{ok, Review} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"hide_review">>, <<"review">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"hide_review">>, <<"review">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:review_to_json(Review));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"Review not found">>)
|
||||
@@ -147,7 +147,7 @@ handle_review(Id, <<"hide">>, Reason, Req, AdminId) ->
|
||||
handle_review(Id, <<"unhide">>, Reason, Req, AdminId) ->
|
||||
case core_review:unhide(Id, Reason) of
|
||||
{ok, Review} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"unhide_review">>, <<"review">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"unhide_review">>, <<"review">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:review_to_json(Review));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"Review not found">>)
|
||||
@@ -158,7 +158,7 @@ handle_review(_Id, _Action, _Reason, Req, _AdminId) ->
|
||||
handle_user(Id, <<"block">>, Reason, Req, AdminId) ->
|
||||
case core_user:block(Id, Reason) of
|
||||
{ok, User} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"block_user">>, <<"user">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"block_user">>, <<"user">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:user_to_json(User));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"User not found">>)
|
||||
@@ -166,7 +166,7 @@ handle_user(Id, <<"block">>, Reason, Req, AdminId) ->
|
||||
handle_user(Id, <<"unblock">>, Reason, Req, AdminId) ->
|
||||
case core_user:unblock(Id, Reason) of
|
||||
{ok, User} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"unblock_user">>, <<"user">>, #{id => Id, reason => Reason}, Req),
|
||||
admin_utils:log_admin_action(AdminId, <<"unblock_user">>, <<"user">>, #{id => Id}, #{reason => Reason}, Req),
|
||||
handler_utils:send_json(Req, 200, handler_utils:user_to_json(User));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req, 404, <<"User not found">>)
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Административный обработчик исторических метрик узлов.
|
||||
%%% GET /v1/admin/nodes/metrics – возвращает массив метрик за период.
|
||||
%%%
|
||||
%%% == Эндпоинт ==
|
||||
%%% ```
|
||||
%%% GET /v1/admin/nodes/metrics?from=ISO8601&to=ISO8601&node=NameOrAll
|
||||
%%% ```
|
||||
%%%
|
||||
%%% == Параметры ==
|
||||
%%% <ul>
|
||||
%%% <li>`from` (обязательный) – начало временного диапазона в ISO8601.</li>
|
||||
%%% <li>`to` (обязательный) – конец временного диапазона в ISO8601.</li>
|
||||
%%% <li>`node` (опциональный) – имя узла Erlang или `<<"all">>` (по умолчанию).
|
||||
%%% При `<<"all">>` возвращаются метрики всех узлов кластера.</li>
|
||||
%%% </ul>
|
||||
%%%
|
||||
%%% == Ответ ==
|
||||
%%% Массив объектов метрик. Каждый объект содержит поля, описанные в `metric_schema/0`.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(admin_handler_node_metrics).
|
||||
@@ -10,115 +25,25 @@
|
||||
|
||||
-include("records.hrl").
|
||||
|
||||
%%% cowboy_handler callback
|
||||
-spec init(cowboy_req:req(), any()) -> {ok, binary(), cowboy_req:req()}.
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Инициализация Cowboy-обработчика.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||
init(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_metrics(Req);
|
||||
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%%% Swagger metadata
|
||||
-spec trails() -> [map()].
|
||||
trails() ->
|
||||
[#{path => <<"/v1/admin/nodes/metrics">>,
|
||||
method => <<"GET">>,
|
||||
description => <<"Get historical node metrics">>,
|
||||
tags => [<<"Node Metrics">>],
|
||||
parameters => [
|
||||
#{name => <<"node">>, in => <<"query">>, schema => #{type => string}, description => <<"Node name, default current">>},
|
||||
#{name => <<"from">>, in => <<"query">>, required => true, schema => #{type => string, format => <<"date-time">>}},
|
||||
#{name => <<"to">>, in => <<"query">>, required => true, schema => #{type => string, format => <<"date-time">>}}
|
||||
],
|
||||
responses => #{
|
||||
200 => #{
|
||||
description => <<"Array of node metrics">>,
|
||||
content => #{<<"application/json">> => #{schema => #{
|
||||
type => array,
|
||||
items => metric_schema()
|
||||
}}}
|
||||
},
|
||||
400 => #{description => <<"Missing required parameters">>},
|
||||
403 => #{description => <<"Admin access required">>}
|
||||
}}
|
||||
].
|
||||
|
||||
metric_schema() ->
|
||||
#{
|
||||
type => object,
|
||||
properties => #{
|
||||
<<"timestamp">> => #{
|
||||
type => string,
|
||||
format => <<"date-time">>,
|
||||
description => <<"Metric collection timestamp (ISO8601)">>
|
||||
},
|
||||
<<"node">> => #{
|
||||
type => string,
|
||||
description => <<"Erlang node name">>
|
||||
},
|
||||
<<"memory_total">> => #{
|
||||
type => integer,
|
||||
description => <<"Total memory allocated by the Erlang VM (bytes)">>
|
||||
},
|
||||
<<"memory_processes">> => #{
|
||||
type => integer,
|
||||
description => <<"Memory used by processes (bytes)">>
|
||||
},
|
||||
<<"memory_atom">> => #{
|
||||
type => integer,
|
||||
description => <<"Memory used by atoms (bytes)">>
|
||||
},
|
||||
<<"memory_binary">> => #{
|
||||
type => integer,
|
||||
description => <<"Memory used by binaries (bytes)">>
|
||||
},
|
||||
<<"memory_ets">> => #{
|
||||
type => integer,
|
||||
description => <<"Memory used by ETS tables (bytes)">>
|
||||
},
|
||||
<<"process_count">> => #{
|
||||
type => integer,
|
||||
description => <<"Number of live Erlang processes">>
|
||||
},
|
||||
<<"run_queue">> => #{
|
||||
type => integer,
|
||||
description => <<"Length of the scheduler run queue">>
|
||||
},
|
||||
<<"mnesia_commits">> => #{
|
||||
type => integer,
|
||||
description => <<"Number of successful Mnesia transactions since node start">>
|
||||
},
|
||||
<<"mnesia_failures">> => #{
|
||||
type => integer,
|
||||
description => <<"Number of failed Mnesia transactions since node start">>
|
||||
},
|
||||
<<"table_sizes">> => #{
|
||||
type => object,
|
||||
description => <<"Map of Mnesia table names to their sizes (number of records)">>,
|
||||
additionalProperties => #{type => integer}
|
||||
},
|
||||
<<"active_sessions">> => #{
|
||||
type => integer,
|
||||
description => <<"Total number of active user and admin sessions">>
|
||||
},
|
||||
<<"ws_connections">> => #{
|
||||
type => integer,
|
||||
description => <<"Number of open WebSocket connections">>
|
||||
},
|
||||
<<"io_in">> => #{
|
||||
type => integer,
|
||||
description => <<"Bytes received on sockets since last collection (delta)">>
|
||||
},
|
||||
<<"io_out">> => #{
|
||||
type => integer,
|
||||
description => <<"Bytes sent over sockets since last collection (delta)">>
|
||||
}
|
||||
}
|
||||
}.
|
||||
|
||||
%%% Internal functions
|
||||
|
||||
%% @doc Возвращает исторические метрики по заданному периоду и узлу.
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Возвращает исторические метрики узлов.
|
||||
%%%
|
||||
%%% При `node=all` (по умолчанию) собирает метрики со всех узлов кластера
|
||||
%%% через RPC-вызовы к удалённым узлам. Локальные метрики извлекаются
|
||||
%%% напрямую.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec get_metrics(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||
get_metrics(Req) ->
|
||||
case handler_utils:auth_admin(Req) of
|
||||
@@ -126,20 +51,49 @@ get_metrics(Req) ->
|
||||
Qs = cowboy_req:parse_qs(Req1),
|
||||
From = handler_utils:parse_datetime_qs(proplists:get_value(<<"from">>, Qs)),
|
||||
To = handler_utils:parse_datetime_qs(proplists:get_value(<<"to">>, Qs)),
|
||||
Node = proplists:get_value(<<"node">>, Qs, node()),
|
||||
NodeParam = proplists:get_value(<<"node">>, Qs, <<"all">>),
|
||||
case {From, To} of
|
||||
{undefined, _} -> handler_utils:send_error(Req1, 400, <<"Missing from">>);
|
||||
{_, undefined} -> handler_utils:send_error(Req1, 400, <<"Missing to">>);
|
||||
{F, T} ->
|
||||
Metrics = core_node_metric:get_by_timerange(F, T, Node),
|
||||
Json = [metric_to_map(M) || M <- Metrics],
|
||||
Metrics = case NodeParam of
|
||||
<<"all">> ->
|
||||
% Локальные метрики текущего узла
|
||||
Local = core_node_metric:get_by_timerange(F, T, node()),
|
||||
% Остальные узлы кластера
|
||||
OtherNodes = mnesia:system_info(db_nodes) -- [node()],
|
||||
io:format("[NODE_METRICS] Aggregating metrics from ~p (local) and remote nodes: ~p~n", [node(), OtherNodes]),
|
||||
Remote = lists:flatmap(fun(N) ->
|
||||
io:format("[NODE_METRICS] Requesting metrics from node ~p...~n", [N]),
|
||||
try rpc:call(N, core_node_metric, get_by_timerange, [F, T, N], 5000) of
|
||||
List when is_list(List) ->
|
||||
io:format("[NODE_METRICS] Received ~p metrics from node ~p~n", [length(List), N]),
|
||||
List;
|
||||
Other ->
|
||||
io:format("[NODE_METRICS] Unexpected response from node ~p: ~p~n", [N, Other]),
|
||||
[]
|
||||
catch
|
||||
Class:Reason:Stacktrace ->
|
||||
io:format("[NODE_METRICS] RPC error for node ~p: ~p:~p~nStacktrace: ~p~n", [N, Class, Reason, Stacktrace]),
|
||||
[]
|
||||
end
|
||||
end, OtherNodes),
|
||||
Local ++ Remote;
|
||||
SpecificNode ->
|
||||
core_node_metric:get_by_timerange(F, T, SpecificNode)
|
||||
end,
|
||||
Sorted = lists:sort(fun(A, B) -> A#node_metric.timestamp =< B#node_metric.timestamp end, Metrics),
|
||||
Json = [metric_to_map(M) || M <- Sorted],
|
||||
handler_utils:send_json(Req1, 200, Json)
|
||||
end;
|
||||
{error, Code, Msg, Req1} ->
|
||||
handler_utils:send_error(Req1, Code, Msg)
|
||||
end.
|
||||
|
||||
%% @private Преобразует запись метрики в JSON-совместимую карту.
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Преобразует запись `#node_metric{}` в JSON-совместимый map.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec metric_to_map(#node_metric{}) -> map().
|
||||
metric_to_map(M) ->
|
||||
#{
|
||||
@@ -150,6 +104,7 @@ metric_to_map(M) ->
|
||||
<<"memory_atom">> => M#node_metric.memory_atom,
|
||||
<<"memory_binary">> => M#node_metric.memory_binary,
|
||||
<<"memory_ets">> => M#node_metric.memory_ets,
|
||||
<<"memory_available">> => M#node_metric.memory_available,
|
||||
<<"process_count">> => M#node_metric.process_count,
|
||||
<<"run_queue">> => M#node_metric.run_queue,
|
||||
<<"mnesia_commits">> => M#node_metric.mnesia_commits,
|
||||
@@ -158,5 +113,63 @@ metric_to_map(M) ->
|
||||
<<"active_sessions">> => M#node_metric.active_sessions,
|
||||
<<"ws_connections">> => M#node_metric.ws_connections,
|
||||
<<"io_in">> => M#node_metric.io_in,
|
||||
<<"io_out">> => M#node_metric.io_out
|
||||
<<"io_out">> => M#node_metric.io_out,
|
||||
<<"cpu_utilization">> => M#node_metric.cpu_utilization
|
||||
}.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Возвращает Swagger-трассы для модуля.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec trails() -> [map()].
|
||||
trails() ->
|
||||
[#{path => <<"/v1/admin/nodes/metrics">>,
|
||||
method => <<"GET">>,
|
||||
description => <<"Get historical node metrics (aggregated from all nodes by default)">>,
|
||||
tags => [<<"Node Metrics">>],
|
||||
parameters => [
|
||||
#{name => <<"node">>, in => <<"query">>, schema => #{type => string},
|
||||
description => <<"Node name or 'all' (default). When 'all', returns metrics from every cluster node.">>},
|
||||
#{name => <<"from">>, in => <<"query">>, required => true,
|
||||
schema => #{type => string, format => <<"date-time">>},
|
||||
description => <<"Start timestamp in ISO8601 format (milliseconds are supported)">>},
|
||||
#{name => <<"to">>, in => <<"query">>, required => true,
|
||||
schema => #{type => string, format => <<"date-time">>},
|
||||
description => <<"End timestamp in ISO8601 format (milliseconds are supported)">>}
|
||||
],
|
||||
responses => #{200 => #{description => <<"Array of node metrics">>,
|
||||
content => #{<<"application/json">> => #{schema => #{
|
||||
type => array,
|
||||
items => metric_schema()
|
||||
}}}}}}
|
||||
].
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Swagger-схема для одного объекта метрики.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec metric_schema() -> map().
|
||||
metric_schema() ->
|
||||
#{
|
||||
type => object,
|
||||
properties => #{
|
||||
<<"timestamp">> => #{type => string, format => <<"date-time">>, description => <<"Timestamp of the metric sample (ISO8601)">>},
|
||||
<<"node">> => #{type => string, description => <<"Erlang node name">>},
|
||||
<<"memory_total">> => #{type => integer, description => <<"Total memory used by Erlang VM (bytes)">>},
|
||||
<<"memory_processes">> => #{type => integer, description => <<"Memory used by processes (bytes)">>},
|
||||
<<"memory_atom">> => #{type => integer, description => <<"Memory used for atoms (bytes)">>},
|
||||
<<"memory_binary">> => #{type => integer, description => <<"Memory used for binary data (bytes)">>},
|
||||
<<"memory_ets">> => #{type => integer, description => <<"Memory used by ETS tables (bytes)">>},
|
||||
<<"memory_available">> => #{type => integer, description => <<"Available system memory (bytes), 0 if unavailable">>},
|
||||
<<"process_count">> => #{type => integer, description => <<"Number of Erlang processes">>},
|
||||
<<"run_queue">> => #{type => integer, description => <<"Run queue length (0 means idle)">>},
|
||||
<<"mnesia_commits">> => #{type => integer, description => <<"Mnesia transaction commits since boot">>},
|
||||
<<"mnesia_failures">> => #{type => integer, description => <<"Mnesia transaction failures since boot">>},
|
||||
<<"table_sizes">> => #{type => object, description => <<"Map of Mnesia table names to record counts">>, additionalProperties => #{type => integer}},
|
||||
<<"active_sessions">> => #{type => integer, description => <<"Number of active user/admin sessions">>},
|
||||
<<"ws_connections">> => #{type => integer, description => <<"Number of active WebSocket connections">>},
|
||||
<<"io_in">> => #{type => integer, description => <<"Delta of bytes received since last sample">>},
|
||||
<<"io_out">> => #{type => integer, description => <<"Delta of bytes sent since last sample">>},
|
||||
<<"cpu_utilization">> => #{type => number, format => float, description => <<"CPU utilization (0.0-100.0 percent)">>}
|
||||
}
|
||||
}.
|
||||
@@ -109,7 +109,7 @@ update_report(Req) ->
|
||||
case logic_report:update_report_status(AdminId, ReportId, StatusAtom) of
|
||||
{ok, Report} ->
|
||||
% Аудит изменения статуса жалобы
|
||||
admin_utils:log_admin_action(AdminId, <<"update_report_status">>, <<"report">>, ReportId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_report_status">>, <<"report">>, ReportId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:report_to_json(Report));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Report not found">>);
|
||||
|
||||
@@ -132,7 +132,7 @@ bulk_update_reviews(Req) ->
|
||||
true = is_list(Operations),
|
||||
case logic_review:bulk_update_status(Operations, Reason) of
|
||||
{ok, UpdatedCount} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"bulk_update_reviews">>, <<"review">>, UpdatedCount, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"bulk_update_reviews">>, <<"review">>, UpdatedCount, Reason, Req2),
|
||||
handler_utils:send_json(Req2, 200, #{updated_count => UpdatedCount});
|
||||
{error, ReasonErr} ->
|
||||
handler_utils:send_error(Req2, 400, ReasonErr)
|
||||
|
||||
@@ -113,7 +113,7 @@ update_review(Req) ->
|
||||
case logic_review:update_review_admin(ReviewId, Converted) of
|
||||
{ok, Review} ->
|
||||
% Аудит изменения отзыва
|
||||
admin_utils:log_admin_action(AdminId, <<"update_review">>, <<"review">>, ReviewId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_review">>, <<"review">>, ReviewId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:review_to_json(Review));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Review not found">>);
|
||||
|
||||
@@ -120,7 +120,7 @@ update_subscription(Req) ->
|
||||
Data when is_map(Data) ->
|
||||
case core_subscription:update_subscription(Id, Data) of
|
||||
{ok, Updated} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"update_subscription">>, <<"subscription">>, Id, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_subscription">>, <<"subscription">>, Id, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:subscription_to_json(Updated));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Subscription not found">>);
|
||||
@@ -142,7 +142,7 @@ delete_subscription(Req) ->
|
||||
Id = cowboy_req:binding(id, Req1),
|
||||
case core_subscription:delete_subscription(Id) of
|
||||
{ok, _} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_subscription">>, <<"subscription">>, Id, Req1),
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_subscription">>, <<"subscription">>, Id, <<>>, Req1),
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Subscription not found">>);
|
||||
|
||||
@@ -131,7 +131,7 @@ update_ticket(Req) ->
|
||||
Result = apply_ticket_changes(AdminId, TicketId, Data),
|
||||
case Result of
|
||||
{ok, Ticket} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"update_ticket">>, <<"ticket">>, TicketId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_ticket">>, <<"ticket">>, TicketId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:ticket_to_json(Ticket));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Ticket not found">>);
|
||||
@@ -157,7 +157,7 @@ delete_ticket(Req) ->
|
||||
TicketId = cowboy_req:binding(id, Req1),
|
||||
case logic_ticket:delete_ticket(AdminId, TicketId) of
|
||||
{ok, _} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_ticket">>, <<"ticket">>, TicketId, Req1),
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_ticket">>, <<"ticket">>, TicketId, <<>>, Req1),
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"Ticket not found">>);
|
||||
|
||||
@@ -122,7 +122,7 @@ update_ticket(Req) ->
|
||||
Result = apply_ticket_changes(AdminId, TicketId, Data),
|
||||
case Result of
|
||||
{ok, Ticket} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"update_ticket">>, <<"ticket">>, TicketId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_ticket">>, <<"ticket">>, TicketId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:ticket_to_json(Ticket));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Ticket not found">>);
|
||||
|
||||
@@ -129,7 +129,7 @@ update_user(Req) ->
|
||||
Converted = convert_user_fields(Updates),
|
||||
case logic_user:update_user_admin(UserId, Converted) of
|
||||
{ok, User} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"update_user">>, <<"user">>, UserId, Req2),
|
||||
admin_utils:log_admin_action(AdminId, <<"update_user">>, <<"user">>, UserId, <<>>, Req2),
|
||||
handler_utils:send_json(Req2, 200, handler_utils:user_to_json(User));
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"User not found">>);
|
||||
@@ -151,7 +151,7 @@ delete_user(Req) ->
|
||||
UserId = cowboy_req:binding(id, Req1),
|
||||
case logic_user:delete_user_admin(UserId) of
|
||||
{ok, _} ->
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_user">>, <<"user">>, UserId, Req1),
|
||||
admin_utils:log_admin_action(AdminId, <<"delete_user">>, <<"user">>, UserId, <<>>, Req1),
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req1, 404, <<"User not found">>);
|
||||
|
||||
@@ -63,7 +63,8 @@ websocket_init(State) ->
|
||||
io:format("[ADMIN_WS] WebSocket initialized for admin ~s~n", [State#state.admin_id]),
|
||||
pg:join(eventhub_admin_ws, self()),
|
||||
% Подписываемся на метрики узлов
|
||||
pg:join(node_metrics, self()),
|
||||
%% pg:join(node_metrics, self()),
|
||||
pg:join(global, node_metrics, self()),
|
||||
{ok, State}.
|
||||
|
||||
%% @doc Обрабатывает входящие текстовые сообщения (subscribe/unsubscribe/ping).
|
||||
@@ -143,5 +144,7 @@ metric_to_map(M) ->
|
||||
<<"active_sessions">> => M#node_metric.active_sessions,
|
||||
<<"ws_connections">> => M#node_metric.ws_connections,
|
||||
<<"io_in">> => M#node_metric.io_in,
|
||||
<<"io_out">> => M#node_metric.io_out
|
||||
<<"io_out">> => M#node_metric.io_out,
|
||||
<<"memory_available">> => M#node_metric.memory_available,
|
||||
<<"cpu_utilization">> => M#node_metric.cpu_utilization
|
||||
}.
|
||||
Reference in New Issue
Block a user