228 lines
7.8 KiB
Erlang
228 lines
7.8 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @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(DEFAULT_PATH_TEMPLATE, "${endpoint_root}/endpoint_log/unmatched_publish.log").
|
|
-define(DEFAULT_ENDPOINT_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 = endpoint_util: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() ->
|
|
Config = log_config(),
|
|
Path = log_path(Config),
|
|
ok = filelib:ensure_dir(Path),
|
|
case disk_log:open([
|
|
{name, ?LOG_NAME},
|
|
{file, Path},
|
|
{type, wrap},
|
|
{format, external},
|
|
{size, {config_pos_integer(max_bytes, Config, ?DEFAULT_MAX_BYTES),
|
|
config_pos_integer(max_files, Config, ?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_config() -> proplists:proplist().
|
|
log_config() ->
|
|
case application:get_env(endpoint, endpoint_log) of
|
|
{ok, Config} when is_list(Config) ->
|
|
Config;
|
|
_ ->
|
|
[]
|
|
end.
|
|
|
|
-spec log_path(proplists:proplist()) -> file:filename_all().
|
|
log_path(Config) ->
|
|
Template = proplists:get_value(path, Config, ?DEFAULT_PATH_TEMPLATE),
|
|
expand_path_template(Template).
|
|
|
|
-spec endpoint_root_dir() -> file:filename_all().
|
|
endpoint_root_dir() ->
|
|
case application:get_env(endpoint, endpoints) of
|
|
{ok, Endpoints} ->
|
|
proplists:get_value(root_dir, Endpoints, ?DEFAULT_ENDPOINT_ROOT_DIR);
|
|
undefined ->
|
|
?DEFAULT_ENDPOINT_ROOT_DIR
|
|
end.
|
|
|
|
-spec config_pos_integer(atom(), proplists:proplist(), pos_integer()) -> pos_integer().
|
|
config_pos_integer(Key, Config, Default) ->
|
|
case proplists:get_value(Key, Config, Default) of
|
|
Value when is_integer(Value), Value > 0 ->
|
|
Value;
|
|
_ ->
|
|
Default
|
|
end.
|
|
|
|
-spec expand_path_template(file:filename_all()) -> file:filename_all().
|
|
expand_path_template(Template) ->
|
|
TemplateBin = iolist_to_binary(Template),
|
|
Vars = path_vars(),
|
|
Expanded = lists:foldl(fun({Name, Value}, Acc) ->
|
|
replace_path_var(Name, Value, Acc)
|
|
end, TemplateBin, Vars),
|
|
binary_to_list(Expanded).
|
|
|
|
-spec path_vars() -> [{binary(), binary()}].
|
|
path_vars() ->
|
|
{{Year, Month, Day}, {Hour, _Minute, _Second}} = calendar:local_time(),
|
|
[
|
|
{<<"endpoint_root">>, iolist_to_binary(endpoint_root_dir())},
|
|
{<<"date">>, format_date(Year, Month, Day)},
|
|
{<<"year">>, integer_to_binary(Year)},
|
|
{<<"month">>, two_digits(Month)},
|
|
{<<"day">>, two_digits(Day)},
|
|
{<<"hour">>, two_digits(Hour)},
|
|
{<<"node">>, atom_to_binary(node(), utf8)}
|
|
].
|
|
|
|
-spec replace_path_var(binary(), binary(), binary()) -> binary().
|
|
replace_path_var(Name, Value, Template) ->
|
|
Template0 = binary:replace(Template, <<"${", Name/binary, "}">>, Value, [global]),
|
|
binary:replace(Template0, <<"{", Name/binary, "}">>, Value, [global]).
|
|
|
|
-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() ->
|
|
MilliSeconds = erlang:system_time(millisecond) rem 1000,
|
|
{{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.~3..0B",
|
|
[Year, Month, Day, Hour, Minute, Second, MilliSeconds]
|
|
)).
|
|
|
|
-spec format_date(integer(), integer(), integer()) -> binary().
|
|
format_date(Year, Month, Day) ->
|
|
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B", [Year, Month, Day])).
|
|
|
|
-spec two_digits(integer()) -> binary().
|
|
two_digits(Value) ->
|
|
iolist_to_binary(io_lib:format("~2..0B", [Value])).
|
|
|
|
-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}.
|