-module(rumCrashLog).
|
|
|
|
%% @doc eRum crash log writer。它将error_logger错误消息以其原始格式发送到`crash_log`指定的文件, 如果`crash_log`未设置则禁用crash logging
|
|
%% Crash logs are printed safely using trunc_io via code mostly lifted from riak_err.
|
|
%% `crashLogMsgSize` 应用程序var用于指定最大值要记录的任何消息的大小。
|
|
%% `crashLogFileSize` 用于指定崩溃日志的最大大小,然后将其旋转(0将禁用)。
|
|
%% `crashLogDate` 基于时间的轮播可通过配置,语法为自述文件中记录了
|
|
%% `crashLogCount` 控制要旋转的文件数已保留。
|
|
|
|
-behaviour(gen_srv).
|
|
|
|
-include("eRum.hrl").
|
|
|
|
-ifdef(TEST).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
-include_lib("kernel/include/file.hrl").
|
|
-endif.
|
|
|
|
-export([
|
|
start/6
|
|
, start_link/6
|
|
|
|
]).
|
|
|
|
-export([
|
|
init/1
|
|
, handleCall/3
|
|
, handleCast/2
|
|
, handleInfo/2
|
|
, terminate/2
|
|
, code_change/3
|
|
]).
|
|
|
|
|
|
-record(state, {
|
|
fileName :: string() %% 文件名
|
|
, fd :: pid() | undefined %% 文件句柄
|
|
, inode :: integer() | undefined %% 文件inode信息
|
|
, ctime :: file:date_time() | undefined %% 文件创建时间
|
|
, maxFmtSize :: integer() %% 单个消息最大字节数
|
|
, maxFileSize :: integer() %% 单个日志文件最大字节数
|
|
, date :: undefined | string() %% 旋转的时间格式
|
|
, count :: integer() %% 要保留的已轮转崩溃日志的数量
|
|
, flap = false :: boolean() %% ???? 这是干啥的呢
|
|
, rotator :: atom() %% 旋转模块实例
|
|
}).
|
|
|
|
start(Filename, MaxFmtSize, MaxFileSize, Date, Count, Rotator) ->
|
|
gen_srv:start({local, ?MODULE}, ?MODULE, {Filename, MaxFmtSize, MaxFileSize, Date, Count, Rotator}, []).
|
|
|
|
start_link(Filename, MaxFmtSize, MaxFileSize, Date, Count, Rotator) ->
|
|
gen_srv:start_link({local, ?MODULE}, ?MODULE, {Filename, MaxFmtSize, MaxFileSize, Date, Count, Rotator}, []).
|
|
|
|
init({RelFilename, MaxFmtSize, MaxFileSize, CfgDate, Count, Rotator} = A) ->
|
|
{ok, Date} = rumUtil:parseRotateSpec(CfgDate),
|
|
Filename = rumUtil:parsePath(RelFilename),
|
|
case Rotator:openLogFile(Filename, false) of
|
|
{ok, Fd, Inode, CTime, _Size} ->
|
|
scheduleRotation(Date),
|
|
{ok, #state{fileName = Filename, fd = Fd, inode = Inode, ctime = CTime, maxFmtSize = MaxFmtSize, maxFileSize = MaxFileSize, date = Date, count = Count, rotator = Rotator}};
|
|
{error, Reason} ->
|
|
?INT_LOG(error, "Failed to open crash log file ~ts with error: ~s", [Filename, file:format_error(Reason)]),
|
|
{ok, #state{fileName = Filename, maxFmtSize = MaxFmtSize, maxFileSize = MaxFileSize, date = Date, count = Count, flap = true, rotator = Rotator}}
|
|
end.
|
|
|
|
handleCall({mWriteLog, Event}, State, _From) ->
|
|
{Reply, NewState} = writeLog(Event, State),
|
|
{reply, Reply, NewState};
|
|
handleCall(_Msg, _State, _From) ->
|
|
?ERR("~p call receive unexpect msg ~p ~n ", [?MODULE, _Msg]),
|
|
{reply, ok}.
|
|
|
|
handleCast(_Msg, _State) ->
|
|
?ERR("~p cast receive unexpect msg ~p ~n ", [?MODULE, _Msg]),
|
|
kpS.
|
|
|
|
handleInfo({mWriteLog, Event}, State) ->
|
|
{_, NewState} = writeLog(Event, State),
|
|
{noreply, NewState};
|
|
handleInfo(mRotate, #state{fileName = Name, count = Count, date = Date, rotator = Rotator}) ->
|
|
_ = Rotator:rotateLogFile(Name, Count),
|
|
scheduleRotation(Date),
|
|
kpS;
|
|
handleInfo(_Msg, _State) ->
|
|
?ERR("~p info receive unexpect msg ~p ~n ", [?MODULE, _Msg]),
|
|
kpS.
|
|
|
|
terminate(_Reason, _State) ->
|
|
ok.
|
|
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
scheduleRotation(undefined) -> ok;
|
|
scheduleRotation(Date) ->
|
|
erlang:send_after(rumUtil:calcNextRotateMs(Date), self(), mRotate),
|
|
ok.
|
|
|
|
otherNodeSuffix(Pid) when node(Pid) =/= node() ->
|
|
PidNode = node(Pid),
|
|
case PidNode =/= node() of
|
|
true ->
|
|
<<"** at node ", (atom_to_binary(node(Pid), utf8))/binary, " **\n">>;
|
|
_ ->
|
|
<<"">>
|
|
end.
|
|
|
|
perhapsSaslReport(error_report, {Pid, Type, Report}, FmtMaxBytes) ->
|
|
case rumStdlib:isErrorReport(Type) of
|
|
true ->
|
|
{saslTypeToReportHead(Type), Pid, saslLimitedStr(Type, Report, FmtMaxBytes), true};
|
|
false ->
|
|
{ignore, ignore, ignore, false}
|
|
end;
|
|
%perhaps_a_sasl_report(info_report, {Pid, Type, Report}, FmtMaxBytes) ->
|
|
%case lager_stdlib:is_my_info_report(Type) of
|
|
%true ->
|
|
%{sasl_type_to_report_head(Type), Pid,
|
|
%sasl_limited_str(Type, Report, FmtMaxBytes), false};
|
|
%false ->
|
|
%{ignore, ignore, ignore, false}
|
|
%end;
|
|
perhapsSaslReport(_, _, _) ->
|
|
{ignore, ignore, ignore, false}.
|
|
|
|
saslTypeToReportHead(supervisor_report) ->
|
|
<<"SUPERVISOR REPORT">>;
|
|
saslTypeToReportHead(crash_report) ->
|
|
<<"CRASH REPORT">>;
|
|
saslTypeToReportHead(progress) ->
|
|
<<"PROGRESS REPORT">>.
|
|
|
|
saslLimitedStr(supervisor_report, Report, FmtMaxBytes) ->
|
|
Name = rumStdlib:sup_get(supervisor, Report),
|
|
Context = rumStdlib:sup_get(errorContext, Report),
|
|
Reason = rumStdlib:sup_get(reason, Report),
|
|
Offender = rumStdlib:sup_get(offender, Report),
|
|
FmtString = <<" Supervisor: ~p~n Context: ~p~n Reason: "
|
|
"~s~n Offender: ~s~n~n">>,
|
|
ReasonStr = eFmt:formatBin(<<"~p">>, [Reason], [{charsLimit, FmtMaxBytes}]),
|
|
OffenderStr = eFmt:formatBin(<<"~p">>, [Offender], [{charsLimit, FmtMaxBytes}]),
|
|
eFmt:format(FmtString, [Name, Context, ReasonStr, OffenderStr]);
|
|
saslLimitedStr(progress, Report, FmtMaxBytes) ->
|
|
[begin
|
|
BinStr = eFmt:formatBin(<<"~p">>, [Data], [{charsLimit, FmtMaxBytes}]),
|
|
eFmt:formatBin(" ~16w: ~s~n", [Tag, BinStr])
|
|
end || {Tag, Data} <- Report];
|
|
saslLimitedStr(crash_report, Report, FmtMaxBytes) ->
|
|
rumStdlib:proc_lib_format(Report, FmtMaxBytes).
|
|
|
|
writeLog(Event, #state{fileName = FileName, fd = FD, inode = Inode, ctime = Ctime, flap = Flap, maxFmtSize = FmtMaxBytes, maxFileSize = RotSize, count = Count, rotator = Rotator} = State) ->
|
|
%% borrowed from riak_err
|
|
{ReportStr, Pid, MsgStr, _ErrorP} =
|
|
case Event of
|
|
{error, _GL, {Pid1, Fmt, Args}} ->
|
|
{<<"ERROR REPORT">>, Pid1, eFmt:formatBin(Fmt, Args, [{charsLimit, FmtMaxBytes}]), true};
|
|
{error_report, _GL, {Pid1, std_error, Rep}} ->
|
|
{<<"ERROR REPORT">>, Pid1, eFmt:formatBin(<<"~p\n">>, [Rep], [{charsLimit, FmtMaxBytes}]), true};
|
|
{error_report, _GL, Other} ->
|
|
perhapsSaslReport(error_report, Other, FmtMaxBytes);
|
|
_ ->
|
|
{ignore, ignore, ignore, false}
|
|
end,
|
|
case ReportStr of
|
|
ignore ->
|
|
{ok, State};
|
|
_ ->
|
|
case Rotator:ensureLogFile(FileName, FD, Inode, Ctime, false) of
|
|
{ok, NewFD, NewInode, NewCtime, FileSize} ->
|
|
case RotSize > 0 andalso FileSize > RotSize of
|
|
true ->
|
|
_ = Rotator:rotateLogFile(FileName, Count),
|
|
handleCast({mWriteLog, Event}, State);
|
|
_ ->
|
|
TimeBinStr = rumUtil:msToBinStr(),
|
|
Time = [TimeBinStr, <<" =">>, ReportStr, <<"====\n">>],
|
|
NodeSuffix = otherNodeSuffix(Pid),
|
|
Msg = eFmt:formatBin("~s~s~s", [Time, MsgStr, NodeSuffix]),
|
|
case file:write(NewFD, unicode:characters_to_binary(Msg)) of
|
|
{error, Reason} when Flap == false ->
|
|
?INT_LOG(error, "Failed to write log message to file ~ts: ~s", [FileName, file:format_error(Reason)]),
|
|
{ok, State#state{fd = NewFD, inode = NewInode, ctime = NewCtime, flap = true}};
|
|
ok ->
|
|
{ok, State#state{fd = NewFD, inode = NewInode, ctime = NewCtime, flap = false}};
|
|
_ ->
|
|
{ok, State#state{fd = NewFD, inode = NewInode, ctime = NewCtime}}
|
|
end
|
|
end;
|
|
{error, Reason} ->
|
|
?IIF(Flap, {ok, State}, begin ?INT_LOG(error, "Failed to reopen crash log ~ts with error: ~s", [FileName, file:format_error(Reason)]), {ok, State#state{flap = true}} end)
|
|
end
|
|
end.
|
|
|
|
|
|
-ifdef(TEST).
|
|
|
|
filesystem_test_() ->
|
|
{foreach,
|
|
fun() ->
|
|
{ok, TestDir} = rumUtil:create_test_dir(),
|
|
CrashLog = filename:join(TestDir, "crash_test.log"),
|
|
ok = rumUtil:safe_write_file(CrashLog, []),
|
|
ok = error_logger:tty(false),
|
|
ok = rumUtil:safe_application_load(lager),
|
|
ok = application:set_env(lager, handlers, [{lager_test_backend, info}]),
|
|
ok = application:set_env(lager, errLoggerRedirect, true),
|
|
ok = application:unset_env(lager, crash_log),
|
|
ok = eRum:start(),
|
|
ok = timer:sleep(1000),
|
|
ok = lager_test_backend:flush(),
|
|
CrashLog
|
|
end,
|
|
fun(_CrashLog) ->
|
|
case whereis(lager_crash_log) of
|
|
P when is_pid(P) ->
|
|
gen_server:stop(P);
|
|
_ ->
|
|
ok
|
|
end,
|
|
ok = application:stop(lager),
|
|
ok = application:stop(goldrush),
|
|
ok = rumUtil:delete_test_dir(),
|
|
ok = error_logger:tty(true)
|
|
end, [
|
|
fun(CrashLog) ->
|
|
{"under normal circumstances, file should be opened",
|
|
fun() ->
|
|
{ok, _} = ?MODULE:start_link(CrashLog, 65535, 0, undefined, 0, lager_rotator_default),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
sync_error_logger:error_msg("Test message\n"),
|
|
{ok, Bin} = file:read_file(CrashLog),
|
|
?assertMatch([_, "Test message\n"], re:split(Bin, "\n", [{return, list}, {parts, 2}]))
|
|
end}
|
|
end,
|
|
fun(CrashLog) ->
|
|
{"file can't be opened on startup triggers an error message",
|
|
fun() ->
|
|
{ok, FInfo0} = file:read_file_info(CrashLog, [raw]),
|
|
FInfo1 = FInfo0#file_info{mode = 0},
|
|
?assertEqual(ok, file:write_file_info(CrashLog, FInfo1)),
|
|
{ok, _} = ?MODULE:start_link(CrashLog, 65535, 0, undefined, 0, lager_rotator_default),
|
|
% Note: required on win32, do this early to prevent subsequent failures
|
|
% from preventing cleanup
|
|
?assertEqual(ok, file:write_file_info(CrashLog, FInfo0)),
|
|
?assertEqual(1, lager_test_backend:count()),
|
|
{_Level, _Time, Message, _Metadata} = lager_test_backend:pop(),
|
|
?assertEqual(
|
|
"Failed to open crash log file " ++ CrashLog ++ " with error: permission denied",
|
|
lists:flatten(Message))
|
|
end}
|
|
end,
|
|
fun(CrashLog) ->
|
|
{"file that becomes unavailable at runtime should trigger an error message",
|
|
fun() ->
|
|
case os:type() of
|
|
{win32, _} ->
|
|
% Note: test is skipped on win32 due to the fact that a file can't be deleted or renamed
|
|
% while a process has an open file handle referencing it
|
|
ok;
|
|
_ ->
|
|
{ok, _} = ?MODULE:start_link(CrashLog, 65535, 0, undefined, 0, lager_rotator_default),
|
|
?assertEqual(0, lager_test_backend:count()),
|
|
sync_error_logger:error_msg("Test message\n"),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
?assertEqual(1, lager_test_backend:count()),
|
|
?assertEqual(ok, file:delete(CrashLog)),
|
|
?assertEqual(ok, rumUtil:safe_write_file(CrashLog, "")),
|
|
{ok, FInfo0} = file:read_file_info(CrashLog, [raw]),
|
|
FInfo1 = FInfo0#file_info{mode = 0},
|
|
?assertEqual(ok, file:write_file_info(CrashLog, FInfo1)),
|
|
sync_error_logger:error_msg("Test message\n"),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
% Note: required on win32, do this early to prevent subsequent failures
|
|
% from preventing cleanup
|
|
?assertEqual(ok, file:write_file_info(CrashLog, FInfo0)),
|
|
?assertEqual(3, lager_test_backend:count()),
|
|
lager_test_backend:pop(),
|
|
{_Level, _Time, Message, _Metadata} = lager_test_backend:pop(),
|
|
?assertEqual(
|
|
"Failed to reopen crash log " ++ CrashLog ++ " with error: permission denied",
|
|
lists:flatten(Message))
|
|
end
|
|
end}
|
|
end,
|
|
fun(CrashLog) ->
|
|
{"unavailable files that are fixed at runtime should start having log messages written",
|
|
fun() ->
|
|
{ok, FInfo} = file:read_file_info(CrashLog, [raw]),
|
|
OldPerms = FInfo#file_info.mode,
|
|
file:write_file_info(CrashLog, FInfo#file_info{mode = 0}),
|
|
{ok, _} = ?MODULE:start_link(CrashLog, 65535, 0, undefined, 0, lager_rotator_default),
|
|
?assertEqual(1, lager_test_backend:count()),
|
|
{_Level, _Time, Message, _Metadata} = lager_test_backend:pop(),
|
|
?assertEqual(
|
|
"Failed to open crash log file " ++ CrashLog ++ " with error: permission denied",
|
|
lists:flatten(Message)),
|
|
file:write_file_info(CrashLog, FInfo#file_info{mode = OldPerms}),
|
|
sync_error_logger:error_msg("Test message~n"),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
{ok, Bin} = file:read_file(CrashLog),
|
|
?assertMatch([_, "Test message\n"], re:split(Bin, "\n", [{return, list}, {parts, 2}]))
|
|
end}
|
|
end,
|
|
fun(CrashLog) ->
|
|
{"external logfile rotation/deletion should be handled",
|
|
fun() ->
|
|
case os:type() of
|
|
{win32, _} ->
|
|
% Note: test is skipped on win32 due to the fact that a file can't be deleted or renamed
|
|
% while a process has an open file handle referencing it
|
|
ok;
|
|
_ ->
|
|
{ok, _} = ?MODULE:start_link(CrashLog, 65535, 0, undefined, 0, lager_rotator_default),
|
|
?assertEqual(0, lager_test_backend:count()),
|
|
sync_error_logger:error_msg("Test message~n"),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
{ok, Bin} = file:read_file(CrashLog),
|
|
?assertMatch([_, "Test message\n"], re:split(Bin, "\n", [{return, list}, {parts, 2}])),
|
|
?assertEqual(ok, file:delete(CrashLog)),
|
|
?assertEqual(ok, rumUtil:safe_write_file(CrashLog, "")),
|
|
sync_error_logger:error_msg("Test message1~n"),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
{ok, Bin1} = file:read_file(CrashLog),
|
|
?assertMatch([_, "Test message1\n"], re:split(Bin1, "\n", [{return, list}, {parts, 2}])),
|
|
file:rename(CrashLog, CrashLog ++ ".0"),
|
|
sync_error_logger:error_msg("Test message2~n"),
|
|
_ = gen_event:which_handlers(error_logger),
|
|
{ok, Bin2} = file:read_file(CrashLog),
|
|
?assertMatch([_, "Test message2\n"], re:split(Bin2, "\n", [{return, list}, {parts, 2}]))
|
|
end
|
|
end}
|
|
end
|
|
]}.
|
|
|
|
-endif.
|
|
|