add endpoint_log

This commit is contained in:
anlicheng 2026-04-23 10:29:24 +08:00
parent df218bfff6
commit bf3942f3d9
3 changed files with 189 additions and 2 deletions

View File

@ -0,0 +1,171 @@
%%%-------------------------------------------------------------------
%%% @doc Endpoint diagnostic log.
%%%
%%% publish
%%% endpoint_subscription
%%%-------------------------------------------------------------------
-module(endpoint_log).
-behaviour(gen_server).
%% API
-export([start_link/0, unmatched_publish/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(LOG_NAME, endpoint_unmatched_publish_log).
-define(LOG_DIR, "endpoint_log").
-define(LOG_FILE, "unmatched_publish.log").
-define(DEFAULT_ROOT_DIR, "log").
-define(DEFAULT_MAX_BYTES, 10485760).
-define(DEFAULT_MAX_FILES, 10).
-define(MAX_LOG_CONTENT_BYTES, 32 * 1024).
-record(state, {
enabled = false :: boolean(),
log_name = ?LOG_NAME :: atom()
}).
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
-spec unmatched_publish(RouteKey :: binary(), Content :: binary()) -> ok.
unmatched_publish(RouteKey, Content) when is_binary(RouteKey), is_binary(Content) ->
case erlang:whereis(?SERVER) of
undefined ->
ok;
Pid when is_pid(Pid) ->
gen_server:cast(Pid, {unmatched_publish, RouteKey, Content}),
ok
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
-spec init(term()) -> {ok, #state{}}.
init([]) ->
ok = iot_log:set_metadata(),
case open_log() of
ok ->
{ok, #state{enabled = true}};
{error, Reason} ->
logger:warning("[endpoint_log] open disk_log failed, reason: ~p", [Reason]),
{ok, #state{enabled = false}}
end.
-spec handle_call(term(), gen_server:from(), #state{}) ->
{reply, ok, #state{}}.
handle_call(_Request, _From, State) ->
{reply, ok, State}.
-spec handle_cast(term(), #state{}) -> {noreply, #state{}}.
handle_cast({unmatched_publish, RouteKey, Content}, State = #state{enabled = true, log_name = LogName}) ->
Entry = encode_unmatched_publish(RouteKey, Content),
case disk_log:blog(LogName, Entry) of
ok ->
{noreply, State};
{error, Reason} ->
logger:warning("[endpoint_log] write unmatched publish log failed, reason: ~p", [Reason]),
{noreply, State#state{enabled = false}}
end;
handle_cast({unmatched_publish, _RouteKey, _Content}, State) ->
{noreply, State};
handle_cast(_Request, State) ->
{noreply, State}.
-spec handle_info(term(), #state{}) -> {noreply, #state{}}.
handle_info(Info, State) ->
logger:debug("[endpoint_log] unknown info: ~p", [Info]),
{noreply, State}.
-spec terminate(term(), #state{}) -> ok.
terminate(_Reason, #state{enabled = true, log_name = LogName}) ->
_ = disk_log:close(LogName),
ok;
terminate(_Reason, _State) ->
ok.
-spec code_change(term() | {down, term()}, #state{}, term()) ->
{ok, #state{}} | {error, term()}.
code_change(_OldVsn, State = #state{}, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec open_log() -> ok | {error, term()}.
open_log() ->
Path = log_path(),
ok = filelib:ensure_dir(Path),
case disk_log:open([
{name, ?LOG_NAME},
{file, Path},
{type, wrap},
{format, external},
{size, {?DEFAULT_MAX_BYTES, ?DEFAULT_MAX_FILES}},
{linkto, self()},
{repair, true}
]) of
{ok, ?LOG_NAME} ->
ok;
{repaired, ?LOG_NAME, _Recovered, _BadBytes} ->
ok;
{error, Reason} ->
{error, Reason}
end.
-spec log_path() -> file:filename_all().
log_path() ->
filename:join([root_dir(), ?LOG_DIR, ?LOG_FILE]).
-spec root_dir() -> file:filename_all().
root_dir() ->
case application:get_env(iot, endpoints) of
{ok, Endpoints} ->
proplists:get_value(root_dir, Endpoints, ?DEFAULT_ROOT_DIR);
undefined ->
?DEFAULT_ROOT_DIR
end.
-spec encode_unmatched_publish(binary(), binary()) -> binary().
encode_unmatched_publish(RouteKey, Content) ->
{LoggedContent, Truncated} = maybe_truncate(Content),
iolist_to_binary([
format_local_time(), <<"\t">>,
<<"unmatched_publish">>, <<"\t">>,
RouteKey, <<"\t">>,
integer_to_binary(byte_size(Content)), <<"\t">>,
boolean_to_binary(Truncated), <<"\t">>,
base64:encode(LoggedContent), <<"\n">>
]).
-spec boolean_to_binary(boolean()) -> binary().
boolean_to_binary(true) ->
<<"true">>;
boolean_to_binary(false) ->
<<"false">>.
-spec format_local_time() -> binary().
format_local_time() ->
{{Year, Month, Day}, {Hour, Minute, Second}} = calendar:local_time(),
iolist_to_binary(io_lib:format(
"~4..0B-~2..0B-~2..0B ~2..0B:~2..0B:~2..0B",
[Year, Month, Day, Hour, Minute, Second]
)).
-spec maybe_truncate(binary()) -> {binary(), boolean()}.
maybe_truncate(Content) when byte_size(Content) =< ?MAX_LOG_CONTENT_BYTES ->
{Content, false};
maybe_truncate(Content) ->
MaxBytes = ?MAX_LOG_CONTENT_BYTES,
<<LoggedContent:MaxBytes/binary, _/binary>> = Content,
{LoggedContent, true}.

View File

@ -55,7 +55,7 @@ unsubscribe(Topic, SubscriberPid) when is_binary(Topic), is_pid(SubscriberPid) -
get_subscribers() ->
gen_server:call(?SERVER, get_subscribers).
-spec publish(RouteKey :: binary(), Content :: binary()) -> no_return().
-spec publish(RouteKey :: binary(), Content :: binary()) -> ok.
publish(RouteKey, Content) when is_binary(RouteKey), is_binary(Content) ->
case ets:info(?SUBSCRIBER_TAB) of
undefined ->
@ -66,6 +66,7 @@ publish(RouteKey, Content) when is_binary(RouteKey), is_binary(Content) ->
lists:foreach(fun(#subscriber{subscriber_pid = SubscriberPid}) ->
endpoint:forward(SubscriberPid, Content)
end, MatchedSubscribers),
maybe_log_unmatched_publish(RouteKey, Content, MatchedSubscribers),
logger:debug("[efka_subscription] route_key: ~p, metric: ~p, match subscribers: ~p", [RouteKey, Content, MatchedSubscribers]),
ok
end.
@ -201,6 +202,12 @@ match_subscribers(Subscribers, Topic) when is_list(Subscribers), is_binary(Topic
Sorted = lists:sort(fun compare_subscriber/2, Matched),
dedupe_subscribers(Sorted).
-spec maybe_log_unmatched_publish(binary(), binary(), [#subscriber{}]) -> ok.
maybe_log_unmatched_publish(RouteKey, Content, []) ->
endpoint_log:unmatched_publish(RouteKey, Content);
maybe_log_unmatched_publish(_RouteKey, _Content, _MatchedSubscribers) ->
ok.
%% topic和发布的topic的Components信息
%% *++
-spec match_components(list(), list()) -> boolean().

View File

@ -28,6 +28,15 @@ start_link() ->
init([]) ->
SupFlags = #{strategy => one_for_all, intensity => 1000, period => 3600},
ChildSpecs = [
#{
id => endpoint_log,
start => {'endpoint_log', start_link, []},
restart => permanent,
shutdown => 2000,
type => worker,
modules => ['endpoint_log']
},
#{
id => endpoint_subscription,
start => {'endpoint_subscription', start_link, []},