%% ------------------------------------------------------------------- %% %% Copyright (c) 2011-2017 Basho Technologies, Inc. %% %% This file is provided to you under the Apache License, %% Version 2.0 (the "License"); you may not use this file %% except in compliance with the License. You may obtain %% a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% %% ------------------------------------------------------------------- %% @doc Lager crash log writer. This module implements a gen_server which writes %% error_logger error messages out to a file in their original format. The %% location to which it logs is configured by the application var `crash_log'. %% Omitting this variable disables crash logging. Crash logs are printed safely %% using trunc_io via code mostly lifted from riak_err. %% %% The `crash_log_msg_size' application var is used to specify the maximum %% size of any message to be logged. `crash_log_size' is used to specify the %% maximum size of the crash log before it will be rotated (0 will disable). %% Time based rotation is configurable via `crash_log_date', the syntax is %% documented in the README. To control the number of rotated files to be %% retained, use `crash_log_count'. -module(lager_crash_log). -include("lager.hrl"). -behaviour(gen_server). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -include_lib("kernel/include/file.hrl"). -endif. %% callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -export([start_link/6, start/6]). -record(state, { name :: string(), fd :: pid() | undefined, inode :: integer() | undefined, ctime :: file:date_time() | undefined, fmtmaxbytes :: integer(), size :: integer(), date :: undefined | string(), count :: integer(), flap=false :: boolean(), rotator :: atom() }). %% @private start_link(Filename, MaxBytes, Size, Date, Count, Rotator) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [Filename, MaxBytes, Size, Date, Count, Rotator], []). %% @private start(Filename, MaxBytes, Size, Date, Count, Rotator) -> gen_server:start({local, ?MODULE}, ?MODULE, [Filename, MaxBytes, Size, Date, Count, Rotator], []). %% @private init([RelFilename, MaxBytes, Size, Date, Count, Rotator]) -> Filename = lager_util:expand_path(RelFilename), case Rotator:open_logfile(Filename, false) of {ok, {FD, Inode, Ctime, _Size}} -> schedule_rotation(Date), {ok, #state{name=Filename, fd=FD, inode=Inode, ctime=Ctime, fmtmaxbytes=MaxBytes, size=Size, count=Count, date=Date, rotator=Rotator}}; {error, Reason} -> ?INT_LOG(error, "Failed to open crash log file ~s with error: ~s", [Filename, file:format_error(Reason)]), {ok, #state{name=Filename, fmtmaxbytes=MaxBytes, flap=true, size=Size, count=Count, date=Date, rotator=Rotator}} end. %% @private handle_call({log, _} = Log, _From, State) -> {Reply, NewState} = do_log(Log, State), {reply, Reply, NewState}; handle_call(_Call, _From, State) -> {reply, ok, State}. %% @private handle_cast({log, _} = Log, State) -> {_, NewState} = do_log(Log, State), {noreply, NewState}; handle_cast(_Request, State) -> {noreply, State}. %% @private handle_info(rotate, #state{name=Name, count=Count, date=Date, rotator=Rotator} = State) -> _ = Rotator:rotate_logfile(Name, Count), schedule_rotation(Date), {noreply, State}; handle_info(_Info, State) -> {noreply, State}. %% @private terminate(_Reason, _State) -> ok. %% @private code_change(_OldVsn, State, _Extra) -> {ok, State}. schedule_rotation(undefined) -> ok; schedule_rotation(Date) -> erlang:send_after(lager_util:calculate_next_rotation(Date) * 1000, self(), rotate), ok. %% ===== Begin code lifted from riak_err ===== -spec limited_fmt(string(), list(), integer()) -> iolist(). %% @doc Format Fmt and Args similar to what io_lib:format/2 does but with %% limits on how large the formatted string may be. %% %% If the Args list's size is larger than TermMaxSize, then the %% formatting is done by trunc_io:print/2, where FmtMaxBytes is used %% to limit the formatted string's size. limited_fmt(Fmt, Args, FmtMaxBytes) -> lager:safe_format(Fmt, Args, FmtMaxBytes). limited_str(Term, FmtMaxBytes) -> {Str, _} = lager_trunc_io:print(Term, FmtMaxBytes), Str. other_node_suffix(Pid) when node(Pid) =/= node() -> "** at node " ++ atom_to_list(node(Pid)) ++ " **\n"; other_node_suffix(_) -> "". perhaps_a_sasl_report(error_report, {Pid, Type, Report}, FmtMaxBytes) -> case lager_stdlib:is_my_error_report(Type) of true -> {sasl_type_to_report_head(Type), Pid, sasl_limited_str(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; perhaps_a_sasl_report(_, _, _) -> {ignore, ignore, ignore, false}. sasl_type_to_report_head(supervisor_report) -> "SUPERVISOR REPORT"; sasl_type_to_report_head(crash_report) -> "CRASH REPORT"; sasl_type_to_report_head(progress) -> "PROGRESS REPORT". sasl_limited_str(supervisor_report, Report, FmtMaxBytes) -> Name = lager_stdlib:sup_get(supervisor, Report), Context = lager_stdlib:sup_get(errorContext, Report), Reason = lager_stdlib:sup_get(reason, Report), Offender = lager_stdlib:sup_get(offender, Report), FmtString = " Supervisor: ~p~n Context: ~p~n Reason: " "~s~n Offender: ~s~n~n", {ReasonStr, _} = lager_trunc_io:print(Reason, FmtMaxBytes), {OffenderStr, _} = lager_trunc_io:print(Offender, FmtMaxBytes), io_lib:format(FmtString, [Name, Context, ReasonStr, OffenderStr]); sasl_limited_str(progress, Report, FmtMaxBytes) -> [begin {Str, _} = lager_trunc_io:print(Data, FmtMaxBytes), io_lib:format(" ~16w: ~s~n", [Tag, Str]) end || {Tag, Data} <- Report]; sasl_limited_str(crash_report, Report, FmtMaxBytes) -> lager_stdlib:proc_lib_format(Report, FmtMaxBytes). do_log({log, Event}, #state{name=Name, fd=FD, inode=Inode, ctime=Ctime, flap=Flap, fmtmaxbytes=FmtMaxBytes, size=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, limited_fmt(Fmt, Args, FmtMaxBytes), true}; {error_report, _GL, {Pid1, std_error, Rep}} -> {"ERROR REPORT", Pid1, limited_str(Rep, FmtMaxBytes) ++ "\n", true}; {error_report, _GL, Other} -> perhaps_a_sasl_report(error_report, Other, FmtMaxBytes); _ -> {ignore, ignore, ignore, false} end, if ReportStr == ignore -> {ok, State}; true -> case Rotator:ensure_logfile(Name, FD, Inode, Ctime, false) of {ok, {_FD, _Inode, _Ctime, Size}} when RotSize /= 0, Size > RotSize -> _ = Rotator:rotate_logfile(Name, Count), handle_cast({log, Event}, State); {ok, {NewFD, NewInode, NewCtime, _Size}} -> {Date, TS} = lager_util:format_time( lager_stdlib:maybe_utc(erlang:localtime())), Time = [Date, " ", TS," =", ReportStr, "====\n"], NodeSuffix = other_node_suffix(Pid), Msg = io_lib:format("~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 ~s: ~s", [Name, 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; {error, Reason} -> case Flap of true -> {ok, State}; _ -> ?INT_LOG(error, "Failed to reopen crash log ~s with error: ~s", [Name, file:format_error(Reason)]), {ok, State#state{flap=true}} end end end. -ifdef(TEST). filesystem_test_() -> {foreach, fun() -> {ok, TestDir} = lager_util:get_test_dir(), CrashLog = filename:join(TestDir, "crash_test.log"), ok = lager_test_util:safe_write_file(CrashLog, []), ok = error_logger:tty(false), ok = lager_test_util:safe_application_load(lager), ok = application:set_env(lager, handlers, [{lager_test_backend, info}]), ok = application:set_env(lager, error_logger_redirect, true), ok = application:unset_env(lager, crash_log), ok = lager: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 = lager_util: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() -> {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()), file:delete(CrashLog), ok = lager_test_util: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, 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() -> {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, lager_test_util: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 ]}. -endif.