@ -0,0 +1,27 @@ | |||||
.rebar3 | |||||
_* | |||||
.eunit | |||||
*.o | |||||
*.beam | |||||
*.plt | |||||
*.swp | |||||
*.swo | |||||
.erlang.cookie | |||||
ebin | |||||
log | |||||
erl_crash.dump | |||||
.rebar | |||||
logs | |||||
_build | |||||
.idea | |||||
*.iml | |||||
rebar3.crashdump | |||||
*~ | |||||
doc | |||||
log | |||||
.local_dialyzer_plt | |||||
dialyzer_unhandled_warnings | |||||
dialyzer_warnings | |||||
rebar.lock | |||||
*.crashdump |
@ -0,0 +1,21 @@ | |||||
MIT License | |||||
Copyright (c) 2020 AICells | |||||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
of this software and associated documentation files (the "Software"), to deal | |||||
in the Software without restriction, including without limitation the rights | |||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
copies of the Software, and to permit persons to whom the Software is | |||||
furnished to do so, subject to the following conditions: | |||||
The above copyright notice and this permission notice shall be included in all | |||||
copies or substantial portions of the Software. | |||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
SOFTWARE. |
@ -0,0 +1,178 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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. | |||||
-define(DEFAULT_TRUNCATION, 4096). | |||||
-define(DEFAULT_TRACER, lager_default_tracer). | |||||
-define(DEFAULT_SINK, lager_event). | |||||
-define(ERROR_LOGGER_SINK, error_logger_lager_event). | |||||
-define(METADATA(Extras), [{severity, info}, | |||||
{pid, self()}, | |||||
{node, node()}, | |||||
{module, ?MODULE}, | |||||
{function, ?FUNCTION_NAME}, | |||||
{function_arity, ?FUNCTION_ARITY}, | |||||
{file, ?FILE}, | |||||
{line, ?LINE} | Extras]). | |||||
-define(lager_log(Severity, Format, Args, Safety), | |||||
?lager_log(?DEFAULT_SINK, Severity, ?METADATA(lager:md()), Format, Args, | |||||
?DEFAULT_TRUNCATION, Safety)). | |||||
-define(lager_log(Severity, Metadata, Format, Args, Safety), | |||||
?lager_log(?DEFAULT_SINK, Severity, ?METADATA(Metadata++lager:md()), Format, Args, | |||||
?DEFAULT_TRUNCATION, Safety)). | |||||
-define(lager_log(Sink, Severity, Metadata, Format, Args, Size, Safety), | |||||
_ = lager:dispatch_log(Sink, Severity, Metadata, Format, Args, Size, Safety)). | |||||
-define(lager_debug(Format, Args), ?lager_log(debug, Format, Args, safe)). | |||||
-define(lager_debug(Metadata, Format, Args), ?lager_log(debug, Metadata, Format, Args, safe)). | |||||
-define(lager_info(Format, Args), ?lager_log(info, Format, Args, safe)). | |||||
-define(lager_info(Metadata, Format, Args), ?lager_log(info, Metadata, Format, Args, safe)). | |||||
-define(lager_notice(Format, Args), ?lager_log(notice, Format, Args, safe)). | |||||
-define(lager_notice(Metadata, Format, Args), ?lager_log(notice, Metadata, Format, Args, safe)). | |||||
-define(lager_warning(Format, Args), ?lager_log(warning, Format, Args, safe)). | |||||
-define(lager_warning(Metadata, Format, Args), ?lager_log(warning, Metadata, Format, Args, safe)). | |||||
-define(lager_error(Format, Args), ?lager_log(error, Format, Args, safe)). | |||||
-define(lager_error(Metadata, Format, Args), ?lager_log(error, Metadata, Format, Args, safe)). | |||||
-define(lager_critical(Format, Args), ?lager_log(critical, Format, Args, safe)). | |||||
-define(lager_critical(Metadata, Format, Args), ?lager_log(critical, Metadata, Format, Args, safe)). | |||||
-define(lager_alert(Format, Args), ?lager_log(alert, Format, Args, safe)). | |||||
-define(lager_alert(Metadata, Format, Args), ?lager_log(alert, Metadata, Format, Args, safe)). | |||||
-define(lager_emergency(Format, Args), ?lager_log(emergency, Format, Args, safe)). | |||||
-define(lager_emergency(Metadata, Format, Args), ?lager_log(emergency, Metadata, Format, Args, safe)). | |||||
-define(lager_none(Format, Args), ?lager_log(none, Format, Args, safe)). | |||||
-define(lager_none(Metadata, Format, Args), ?lager_log(none, Metadata, Format, Args, safe)). | |||||
-define(LEVELS, | |||||
[debug, info, notice, warning, error, critical, alert, emergency, none]). | |||||
%% Use of these "functions" means that the argument list will not be | |||||
%% truncated for safety | |||||
-define(LEVELS_UNSAFE, | |||||
[{debug_unsafe, debug}, {info_unsafe, info}, {notice_unsafe, notice}, {warning_unsafe, warning}, {error_unsafe, error}, {critical_unsafe, critical}, {alert_unsafe, alert}, {emergency_unsafe, emergency}]). | |||||
-define(DEBUG, 128). | |||||
-define(INFO, 64). | |||||
-define(NOTICE, 32). | |||||
-define(WARNING, 16). | |||||
-define(ERROR, 8). | |||||
-define(CRITICAL, 4). | |||||
-define(ALERT, 2). | |||||
-define(EMERGENCY, 1). | |||||
-define(LOG_NONE, 0). | |||||
-define(LEVEL2NUM(Level), | |||||
case Level of | |||||
debug -> ?DEBUG; | |||||
info -> ?INFO; | |||||
notice -> ?NOTICE; | |||||
warning -> ?WARNING; | |||||
error -> ?ERROR; | |||||
critical -> ?CRITICAL; | |||||
alert -> ?ALERT; | |||||
emergency -> ?EMERGENCY | |||||
end). | |||||
-define(NUM2LEVEL(Num), | |||||
case Num of | |||||
?DEBUG -> debug; | |||||
?INFO -> info; | |||||
?NOTICE -> notice; | |||||
?WARNING -> warning; | |||||
?ERROR -> error; | |||||
?CRITICAL -> critical; | |||||
?ALERT -> alert; | |||||
?EMERGENCY -> emergency | |||||
end). | |||||
-define(SHOULD_LOG(Sink, Level), | |||||
(lager_util:level_to_num(Level) band element(1, lager_config:get({Sink, loglevel}, {?LOG_NONE, []}))) /= 0). | |||||
-define(SHOULD_LOG(Level), | |||||
(lager_util:level_to_num(Level) band element(1, lager_config:get(loglevel, {?LOG_NONE, []}))) /= 0). | |||||
-define(NOTIFY(Level, Pid, Format, Args), | |||||
gen_event:notify(lager_event, {log, lager_msg:new(io_lib:format(Format, Args), | |||||
Level, | |||||
[{pid,Pid},{line,?LINE},{file,?FILE},{module,?MODULE}], | |||||
[])} | |||||
)). | |||||
%% FOR INTERNAL USE ONLY | |||||
%% internal non-blocking logging call | |||||
%% there's some special handing for when we try to log (usually errors) while | |||||
%% lager is still starting. | |||||
-ifdef(TEST). | |||||
-define(INT_LOG(Level, Format, Args), | |||||
case ?SHOULD_LOG(Level) of | |||||
true -> | |||||
?NOTIFY(Level, self(), Format, Args); | |||||
_ -> | |||||
ok | |||||
end). | |||||
-else. | |||||
-define(INT_LOG(Level, Format, Args), | |||||
Self = self(), | |||||
%% do this in a spawn so we don't cause a deadlock calling gen_event:which_handlers | |||||
%% from a gen_event handler | |||||
spawn(fun() -> | |||||
case catch(gen_event:which_handlers(lager_event)) of | |||||
X when X == []; X == {'EXIT', noproc}; X == [lager_backend_throttle] -> | |||||
%% there's no handlers yet or lager isn't running, try again | |||||
%% in half a second. | |||||
timer:sleep(500), | |||||
?NOTIFY(Level, Self, Format, Args); | |||||
_ -> | |||||
case ?SHOULD_LOG(Level) of | |||||
true -> | |||||
?NOTIFY(Level, Self, Format, Args); | |||||
_ -> | |||||
ok | |||||
end | |||||
end | |||||
end)). | |||||
-endif. | |||||
-record(lager_shaper, { | |||||
id :: any(), | |||||
%% how many messages per second we try to deliver | |||||
hwm = undefined :: 'undefined' | pos_integer(), | |||||
%% how many messages we've received this second | |||||
mps = 0 :: non_neg_integer(), | |||||
%% the current second | |||||
lasttime = os:timestamp() :: erlang:timestamp(), | |||||
%% count of dropped messages this second | |||||
dropped = 0 :: non_neg_integer(), | |||||
%% If true, flush notify messages from msg queue at overload | |||||
flush_queue = true :: boolean(), | |||||
flush_threshold = 0 :: integer(), | |||||
%% timer | |||||
timer = make_ref() :: reference(), | |||||
%% optional filter fun to avoid counting suppressed messages against HWM totals | |||||
filter = fun(_) -> false end :: fun() | |||||
}). | |||||
-type lager_shaper() :: #lager_shaper{}. |
@ -0,0 +1,44 @@ | |||||
{erl_opts, [ | |||||
{lager_extra_sinks, ['__lager_test_sink']}, | |||||
{platform_define, "^(19|20|21|22)", test_statem}, | |||||
{platform_define, "^18", 'FUNCTION_NAME', unavailable}, | |||||
{platform_define, "^18", 'FUNCTION_ARITY', 0}, | |||||
debug_info, | |||||
report, | |||||
verbose, | |||||
warn_deprecated_function, | |||||
warn_deprecated_type, | |||||
warn_export_all, | |||||
warn_export_vars, | |||||
warn_obsolete_guard, | |||||
warn_untyped_record, | |||||
warn_unused_import | |||||
% do NOT include warnings_as_errors, as rebar includes these options | |||||
% when compiling for eunit, and at least one test module has code that | |||||
% is deliberatly broken and will generate an un-maskable warning | |||||
]}. | |||||
{erl_first_files, ["src/lager_util.erl"]}. | |||||
{eunit_opts, [verbose]}. | |||||
{eunit_compile_opts, [ | |||||
export_all, | |||||
nowarn_untyped_record, | |||||
nowarn_export_all | |||||
]}. | |||||
{deps, [ | |||||
{goldrush, "0.1.9"} | |||||
]}. | |||||
{shell, [ | |||||
% {config, "config/sys.config"}, | |||||
{apps, [eRum]} | |||||
]}. | |||||
{xref_checks, []}. | |||||
{xref_queries, [{"(XC - UC) || (XU - X - B - lager_default_tracer : Mod - erlang:\"(is_map|map_size)\"/1 - maps:to_list/1)", []}]}. | |||||
{cover_enabled, true}. | |||||
{edoc_opts, [{stylesheet_file, "./priv/edoc.css"}]}. |
@ -0,0 +1,15 @@ | |||||
{application, eRum, | |||||
[{description, "An OTP application"}, | |||||
{vsn, "0.1.0"}, | |||||
{registered, []}, | |||||
{mod, {eRum_app, []}}, | |||||
{applications, | |||||
[kernel, | |||||
stdlib | |||||
]}, | |||||
{env,[]}, | |||||
{modules, []}, | |||||
{licenses, ["Apache 2.0"]}, | |||||
{links, []} | |||||
]}. |
@ -0,0 +1,18 @@ | |||||
%%%------------------------------------------------------------------- | |||||
%% @doc eRum public API | |||||
%% @end | |||||
%%%------------------------------------------------------------------- | |||||
-module(eRum_app). | |||||
-behaviour(application). | |||||
-export([start/2, stop/1]). | |||||
start(_StartType, _StartArgs) -> | |||||
eRum_sup:start_link(). | |||||
stop(_State) -> | |||||
ok. | |||||
%% internal functions |
@ -0,0 +1,35 @@ | |||||
%%%------------------------------------------------------------------- | |||||
%% @doc eRum top level supervisor. | |||||
%% @end | |||||
%%%------------------------------------------------------------------- | |||||
-module(eRum_sup). | |||||
-behaviour(supervisor). | |||||
-export([start_link/0]). | |||||
-export([init/1]). | |||||
-define(SERVER, ?MODULE). | |||||
start_link() -> | |||||
supervisor:start_link({local, ?SERVER}, ?MODULE, []). | |||||
%% sup_flags() = #{strategy => strategy(), % optional | |||||
%% intensity => non_neg_integer(), % optional | |||||
%% period => pos_integer()} % optional | |||||
%% child_spec() = #{id => child_id(), % mandatory | |||||
%% start => mfargs(), % mandatory | |||||
%% restart => restart(), % optional | |||||
%% shutdown => shutdown(), % optional | |||||
%% type => worker(), % optional | |||||
%% modules => modules()} % optional | |||||
init([]) -> | |||||
SupFlags = #{strategy => one_for_all, | |||||
intensity => 0, | |||||
period => 1}, | |||||
ChildSpecs = [], | |||||
{ok, {SupFlags, ChildSpecs}}. | |||||
%% internal functions |
@ -0,0 +1,794 @@ | |||||
%% Copyright (c) 2011-2015 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 A error_logger backend for redirecting events into lager. | |||||
%% Error messages and crash logs are also optionally written to a crash log. | |||||
%% @see lager_crash_log | |||||
%% @private | |||||
-module(error_logger_lager_h). | |||||
-include("lager.hrl"). | |||||
-behaviour(gen_event). | |||||
-export([set_high_water/1]). | |||||
-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, | |||||
code_change/3]). | |||||
-export([format_reason/1, format_mfa/1, format_args/3]). | |||||
-record(state, { | |||||
sink :: atom(), | |||||
shaper :: lager_shaper(), | |||||
%% group leader strategy | |||||
groupleader_strategy :: handle | ignore | mirror, | |||||
raw :: boolean() | |||||
}). | |||||
-define(LOGMSG(Sink, Level, Pid, Msg), | |||||
case ?SHOULD_LOG(Sink, Level) of | |||||
true -> | |||||
_ =lager:log(Sink, Level, Pid, Msg, []), | |||||
logged; | |||||
_ -> no_log | |||||
end). | |||||
-define(LOGFMT(Sink, Level, Pid, Fmt, Args), | |||||
case ?SHOULD_LOG(Sink, Level) of | |||||
true -> | |||||
_ = lager:log(Sink, Level, Pid, Fmt, Args), | |||||
logged; | |||||
_ -> no_log | |||||
end). | |||||
-ifdef(TEST). | |||||
%% Make CRASH synchronous when testing, to avoid timing headaches | |||||
-define(CRASH_LOG(Event), | |||||
catch(gen_server:call(lager_crash_log, {log, Event}))). | |||||
-else. | |||||
-define(CRASH_LOG(Event), | |||||
gen_server:cast(lager_crash_log, {log, Event})). | |||||
-endif. | |||||
set_high_water(N) -> | |||||
gen_event:call(error_logger, ?MODULE, {set_high_water, N}, infinity). | |||||
-spec init(any()) -> {ok, #state{}}. | |||||
init([HighWaterMark, GlStrategy]) -> | |||||
Flush = application:get_env(lager, error_logger_flush_queue, true), | |||||
FlushThr = application:get_env(lager, error_logger_flush_threshold, 0), | |||||
Shaper = #lager_shaper{hwm=HighWaterMark, flush_queue = Flush, flush_threshold = FlushThr, filter=shaper_fun(), id=?MODULE}, | |||||
Raw = application:get_env(lager, error_logger_format_raw, false), | |||||
Sink = configured_sink(), | |||||
{ok, #state{sink=Sink, shaper=Shaper, groupleader_strategy=GlStrategy, raw=Raw}}. | |||||
handle_call({set_high_water, N}, #state{shaper=Shaper} = State) -> | |||||
NewShaper = Shaper#lager_shaper{hwm=N}, | |||||
{ok, ok, State#state{shaper = NewShaper}}; | |||||
handle_call(_Request, State) -> | |||||
{ok, unknown_call, State}. | |||||
shaper_fun() -> | |||||
case {application:get_env(lager, suppress_supervisor_start_stop, false), application:get_env(lager, suppress_application_start_stop, false)} of | |||||
{false, false} -> | |||||
fun(_) -> false end; | |||||
{true, true} -> | |||||
fun suppress_supervisor_start_and_application_start/1; | |||||
{false, true} -> | |||||
fun suppress_application_start/1; | |||||
{true, false} -> | |||||
fun suppress_supervisor_start/1 | |||||
end. | |||||
suppress_supervisor_start_and_application_start(E) -> | |||||
suppress_supervisor_start(E) orelse suppress_application_start(E). | |||||
suppress_application_start({info_report, _GL, {_Pid, std_info, D}}) when is_list(D) -> | |||||
lists:member({exited, stopped}, D); | |||||
suppress_application_start({info_report, _GL, {_P, progress, D}}) -> | |||||
lists:keymember(application, 1, D) andalso lists:keymember(started_at, 1, D); | |||||
suppress_application_start(_) -> | |||||
false. | |||||
suppress_supervisor_start({info_report, _GL, {_P, progress, D}}) -> | |||||
lists:keymember(started, 1, D) andalso lists:keymember(supervisor, 1, D); | |||||
suppress_supervisor_start(_) -> | |||||
false. | |||||
handle_event(Event, #state{sink=Sink, shaper=Shaper} = State) -> | |||||
case lager_util:check_hwm(Shaper, Event) of | |||||
{true, 0, NewShaper} -> | |||||
eval_gl(Event, State#state{shaper=NewShaper}); | |||||
{true, Drop, #lager_shaper{hwm=Hwm} = NewShaper} when Drop > 0 -> | |||||
?LOGFMT(Sink, warning, self(), | |||||
"lager_error_logger_h dropped ~p messages in the last second that exceeded the limit of ~p messages/sec", | |||||
[Drop, Hwm]), | |||||
eval_gl(Event, State#state{shaper=NewShaper}); | |||||
{false, _, #lager_shaper{dropped=D} = NewShaper} -> | |||||
{ok, State#state{shaper=NewShaper#lager_shaper{dropped=D+1}}} | |||||
end. | |||||
handle_info({shaper_expired, ?MODULE}, #state{sink=Sink, shaper=Shaper} = State) -> | |||||
case Shaper#lager_shaper.dropped of | |||||
0 -> | |||||
ok; | |||||
Dropped -> | |||||
?LOGFMT(Sink, warning, self(), | |||||
"lager_error_logger_h dropped ~p messages in the last second that exceeded the limit of ~p messages/sec", | |||||
[Dropped, Shaper#lager_shaper.hwm]) | |||||
end, | |||||
{ok, State#state{shaper=Shaper#lager_shaper{dropped=0, mps=0, lasttime=os:timestamp()}}}; | |||||
handle_info(_Info, State) -> | |||||
{ok, State}. | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
code_change(_OldVsn, {state, Shaper, GLStrategy}, _Extra) -> | |||||
Raw = application:get_env(lager, error_logger_format_raw, false), | |||||
{ok, #state{ | |||||
sink=configured_sink(), | |||||
shaper=Shaper, | |||||
groupleader_strategy=GLStrategy, | |||||
raw=Raw | |||||
}}; | |||||
code_change(_OldVsn, {state, Sink, Shaper, GLS}, _Extra) -> | |||||
Raw = application:get_env(lager, error_logger_format_raw, false), | |||||
{ok, #state{sink=Sink, shaper=Shaper, groupleader_strategy=GLS, raw=Raw}}; | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. | |||||
%% internal functions | |||||
configured_sink() -> | |||||
case proplists:get_value(?ERROR_LOGGER_SINK, application:get_env(lager, extra_sinks, [])) of | |||||
undefined -> ?DEFAULT_SINK; | |||||
_ -> ?ERROR_LOGGER_SINK | |||||
end. | |||||
eval_gl(Event, #state{groupleader_strategy=GlStrategy0}=State) when is_pid(element(2, Event)) -> | |||||
case element(2, Event) of | |||||
GL when node(GL) =/= node(), GlStrategy0 =:= ignore -> | |||||
gen_event:notify({error_logger, node(GL)}, Event), | |||||
{ok, State}; | |||||
GL when node(GL) =/= node(), GlStrategy0 =:= mirror -> | |||||
gen_event:notify({error_logger, node(GL)}, Event), | |||||
log_event(Event, State); | |||||
_ -> | |||||
log_event(Event, State) | |||||
end; | |||||
eval_gl(Event, State) -> | |||||
log_event(Event, State). | |||||
log_event(Event, #state{sink=Sink} = State) -> | |||||
DidLog = case Event of | |||||
{error, _GL, {Pid, Fmt, Args}} -> | |||||
FormatRaw = State#state.raw, | |||||
case {FormatRaw, Fmt} of | |||||
{false, "** Generic server "++_} -> | |||||
%% gen_server terminate | |||||
{Reason, Name} = case Args of | |||||
[N, _Msg, _State, R] -> | |||||
{R, N}; | |||||
[N, _Msg, _State, R, _Client] -> | |||||
%% OTP 20 crash reports where the client pid is dead don't include the stacktrace | |||||
{R, N}; | |||||
[N, _Msg, _State, R, _Client, _Stacktrace] -> | |||||
%% OTP 20 crash reports contain the pid of the client and stacktrace | |||||
%% TODO do something with them | |||||
{R, N} | |||||
end, | |||||
?CRASH_LOG(Event), | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
?LOGFMT(Sink, error, [{pid, Pid}, {name, Name} | Md], "gen_server ~w terminated with reason: ~s", | |||||
[Name, Formatted]); | |||||
{false, "** State machine "++_} -> | |||||
%% Check if the terminated process is gen_fsm or gen_statem | |||||
%% since they generate the same exit message | |||||
{Type, Name, StateName, Reason} = case Args of | |||||
[TName, _Msg, TStateName, _StateData, TReason] -> | |||||
{gen_fsm, TName, TStateName, TReason}; | |||||
%% Handle changed logging in gen_fsm stdlib-3.9 (TPid, ClientArgs) | |||||
[TName, _Msg, TPid, TStateName, _StateData, TReason | _ClientArgs] when is_pid(TPid), is_atom(TStateName) -> | |||||
{gen_fsm, TName, TStateName, TReason}; | |||||
%% Handle changed logging in gen_statem stdlib-3.9 (ClientArgs) | |||||
[TName, _Msg, {TStateName, _StateData}, _ExitType, TReason, _CallbackMode, Stacktrace | _ClientArgs] -> | |||||
{gen_statem, TName, TStateName, {TReason, Stacktrace}}; | |||||
%% Handle changed logging in gen_statem stdlib-3.9 (ClientArgs) | |||||
[TName, {TStateName, _StateData}, _ExitType, TReason, _CallbackMode, Stacktrace | _ClientArgs] -> | |||||
{gen_statem, TName, TStateName, {TReason, Stacktrace}}; | |||||
[TName, _Msg, [{TStateName, _StateData}], _ExitType, TReason, _CallbackMode, Stacktrace | _ClientArgs] -> | |||||
%% sometimes gen_statem wraps its statename/data in a list for some reason??? | |||||
{gen_statem, TName, TStateName, {TReason, Stacktrace}} | |||||
end, | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
?CRASH_LOG(Event), | |||||
?LOGFMT(Sink, error, [{pid, Pid}, {name, Name} | Md], "~s ~w in state ~w terminated with reason: ~s", | |||||
[Type, Name, StateName, Formatted]); | |||||
{false, "** gen_event handler"++_} -> | |||||
%% gen_event handler terminate | |||||
[ID, Name, _Msg, _State, Reason] = Args, | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
?CRASH_LOG(Event), | |||||
?LOGFMT(Sink, error, [{pid, Pid}, {name, Name} | Md], "gen_event ~w installed in ~w terminated with reason: ~s", | |||||
[ID, Name, Formatted]); | |||||
{false, "** Cowboy handler"++_} -> | |||||
%% Cowboy HTTP server error | |||||
?CRASH_LOG(Event), | |||||
case Args of | |||||
[Module, Function, Arity, _Request, _State] -> | |||||
%% we only get the 5-element list when its a non-exported function | |||||
?LOGFMT(Sink, error, Pid, | |||||
"Cowboy handler ~p terminated with reason: call to undefined function ~p:~p/~p", | |||||
[Module, Module, Function, Arity]); | |||||
[Module, Function, Arity, _Class, Reason | Tail] -> | |||||
%% any other cowboy error_format list *always* ends with the stacktrace | |||||
StackTrace = lists:last(Tail), | |||||
{Md, Formatted} = format_reason_md({Reason, StackTrace}), | |||||
?LOGFMT(Sink, error, [{pid, Pid} | Md], | |||||
"Cowboy handler ~p terminated in ~p:~p/~p with reason: ~s", | |||||
[Module, Module, Function, Arity, Formatted]) | |||||
end; | |||||
{false, "Ranch listener "++_} -> | |||||
%% Ranch errors | |||||
?CRASH_LOG(Event), | |||||
case Args of | |||||
%% Error logged by cowboy, which starts as ranch error | |||||
[Ref, ConnectionPid, StreamID, RequestPid, Reason, StackTrace] -> | |||||
{Md, Formatted} = format_reason_md({Reason, StackTrace}), | |||||
?LOGFMT(Sink, error, [{pid, RequestPid} | Md], | |||||
"Cowboy stream ~p with ranch listener ~p and connection process ~p " | |||||
"had its request process exit with reason: ~s", | |||||
[StreamID, Ref, ConnectionPid, Formatted]); | |||||
[Ref, _Protocol, Worker, {[{reason, Reason}, {mfa, {Module, Function, Arity}}, {stacktrace, StackTrace} | _], _}] -> | |||||
{Md, Formatted} = format_reason_md({Reason, StackTrace}), | |||||
?LOGFMT(Sink, error, [{pid, Worker} | Md], | |||||
"Ranch listener ~p terminated in ~p:~p/~p with reason: ~s", | |||||
[Ref, Module, Function, Arity, Formatted]); | |||||
[Ref, _Protocol, Worker, Reason] -> | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
?LOGFMT(Sink, error, [{pid, Worker} | Md], | |||||
"Ranch listener ~p terminated with reason: ~s", | |||||
[Ref, Formatted]); | |||||
[Ref, Protocol, Ret] -> | |||||
%% ranch_conns_sup.erl module line 119-123 has three parameters error msg, log it. | |||||
{Md, Formatted} = format_reason_md(Ret), | |||||
?LOGFMT(Sink, error, [{pid, Protocol} | Md], | |||||
"Ranch listener ~p terminated with result:~s", | |||||
[Ref, Formatted]) | |||||
end; | |||||
{false, "webmachine error"++_} -> | |||||
%% Webmachine HTTP server error | |||||
?CRASH_LOG(Event), | |||||
[Path, Error] = Args, | |||||
%% webmachine likes to mangle the stack, for some reason | |||||
StackTrace = case Error of | |||||
{error, {error, Reason, Stack}} -> | |||||
{Reason, Stack}; | |||||
_ -> | |||||
Error | |||||
end, | |||||
{Md, Formatted} = format_reason_md(StackTrace), | |||||
?LOGFMT(Sink, error, [{pid, Pid} | Md], "Webmachine error at path ~p : ~s", [Path, Formatted]); | |||||
_ -> | |||||
?CRASH_LOG(Event), | |||||
?LOGFMT(Sink, error, Pid, Fmt, Args) | |||||
end; | |||||
{error_report, _GL, {Pid, std_error, D}} -> | |||||
?CRASH_LOG(Event), | |||||
?LOGMSG(Sink, error, Pid, print_silly_list(D)); | |||||
{error_report, _GL, {Pid, supervisor_report, D}} -> | |||||
?CRASH_LOG(Event), | |||||
case lists:sort(D) of | |||||
[{errorContext, Ctx}, {offender, Off}, {reason, Reason}, {supervisor, Name}] -> | |||||
Offender = format_offender(Off), | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
?LOGFMT(Sink, error, [{pid, Pid} | Md], | |||||
"Supervisor ~w had child ~s exit with reason ~s in context ~w", | |||||
[supervisor_name(Name), Offender, Formatted, Ctx]); | |||||
_ -> | |||||
?LOGMSG(Sink, error, Pid, "SUPERVISOR REPORT " ++ print_silly_list(D)) | |||||
end; | |||||
{error_report, _GL, {Pid, crash_report, [Self, Neighbours]}} -> | |||||
?CRASH_LOG(Event), | |||||
{Md, Formatted} = format_crash_report(Self, Neighbours), | |||||
?LOGMSG(Sink, error, [{pid, Pid} | Md], "CRASH REPORT " ++ Formatted); | |||||
{warning_msg, _GL, {Pid, Fmt, Args}} -> | |||||
?LOGFMT(Sink, warning, Pid, Fmt, Args); | |||||
{warning_report, _GL, {Pid, std_warning, Report}} -> | |||||
?LOGMSG(Sink, warning, Pid, print_silly_list(Report)); | |||||
{info_msg, _GL, {Pid, Fmt, Args}} -> | |||||
?LOGFMT(Sink, info, Pid, Fmt, Args); | |||||
{info_report, _GL, {Pid, std_info, D}} when is_list(D) -> | |||||
Details = lists:sort(D), | |||||
case Details of | |||||
[{application, App}, {exited, Reason}, {type, _Type}] -> | |||||
case application:get_env(lager, suppress_application_start_stop, false) of | |||||
true when Reason == stopped -> | |||||
no_log; | |||||
_ -> | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
?LOGFMT(Sink, info, [{pid, Pid} | Md], "Application ~w exited with reason: ~s", | |||||
[App, Formatted]) | |||||
end; | |||||
_ -> | |||||
?LOGMSG(Sink, info, Pid, print_silly_list(D)) | |||||
end; | |||||
{info_report, _GL, {Pid, std_info, D}} -> | |||||
?LOGFMT(Sink, info, Pid, "~w", [D]); | |||||
{info_report, _GL, {P, progress, D}} -> | |||||
Details = lists:sort(D), | |||||
case Details of | |||||
[{application, App}, {started_at, Node}] -> | |||||
case application:get_env(lager, suppress_application_start_stop, false) of | |||||
true -> | |||||
no_log; | |||||
_ -> | |||||
?LOGFMT(Sink, info, P, "Application ~w started on node ~w", | |||||
[App, Node]) | |||||
end; | |||||
[{started, Started}, {supervisor, Name}] -> | |||||
case application:get_env(lager, suppress_supervisor_start_stop, false) of | |||||
true -> | |||||
no_log; | |||||
_ -> | |||||
MFA = format_mfa(get_value(mfargs, Started)), | |||||
Pid = get_value(pid, Started), | |||||
?LOGFMT(Sink, debug, P, "Supervisor ~w started ~s at pid ~w", | |||||
[supervisor_name(Name), MFA, Pid]) | |||||
end; | |||||
_ -> | |||||
?LOGMSG(Sink, info, P, "PROGRESS REPORT " ++ print_silly_list(D)) | |||||
end; | |||||
_ -> | |||||
?LOGFMT(Sink, warning, self(), "Unexpected error_logger event ~w", [Event]) | |||||
end, | |||||
case DidLog of | |||||
logged -> | |||||
{ok, State}; | |||||
no_log -> | |||||
Shaper = State#state.shaper, | |||||
{ok, State#state{ | |||||
shaper = Shaper#lager_shaper{ | |||||
mps = Shaper#lager_shaper.mps - 1 | |||||
} | |||||
} | |||||
} | |||||
end. | |||||
format_crash_report(Report, Neighbours) -> | |||||
Name = case get_value(registered_name, Report, []) of | |||||
[] -> | |||||
%% process_info(Pid, registered_name) returns [] for unregistered processes | |||||
get_value(pid, Report); | |||||
Atom -> Atom | |||||
end, | |||||
Md0 = case get_value(dictionary, Report, []) of | |||||
[] -> | |||||
%% process_info(Pid, registered_name) returns [] for unregistered processes | |||||
[]; | |||||
Dict -> | |||||
%% pull the lager metadata out of the process dictionary, if we can | |||||
get_value('_lager_metadata', Dict, []) | |||||
end, | |||||
{Class, Reason, Trace} = get_value(error_info, Report), | |||||
{Md, ReasonStr} = format_reason_md({Reason, Trace}), | |||||
Type = case Class of | |||||
exit -> "exited"; | |||||
_ -> "crashed" | |||||
end, | |||||
{Md0 ++ Md, io_lib:format("Process ~w with ~w neighbours ~s with reason: ~s", | |||||
[Name, length(Neighbours), Type, ReasonStr])}. | |||||
format_offender(Off) -> | |||||
case get_value(mfargs, Off) of | |||||
undefined -> | |||||
%% supervisor_bridge | |||||
io_lib:format("at module ~w at ~w", | |||||
[get_value(mod, Off), get_value(pid, Off)]); | |||||
MFArgs -> | |||||
%% regular supervisor | |||||
{_, MFA} = format_mfa_md(MFArgs), | |||||
%% In 2014 the error report changed from `name' to | |||||
%% `id', so try that first. | |||||
Name = case get_value(id, Off) of | |||||
undefined -> | |||||
get_value(name, Off); | |||||
Id -> | |||||
Id | |||||
end, | |||||
io_lib:format("~p started with ~s at ~w", | |||||
[Name, MFA, get_value(pid, Off)]) | |||||
end. | |||||
%% backwards compatability shim | |||||
format_reason(Reason) -> | |||||
element(2, format_reason_md(Reason)). | |||||
-spec format_reason_md(Stacktrace:: any()) -> {Metadata:: [{atom(), any()}], String :: list()}. | |||||
format_reason_md({'function not exported', [{M, F, A},MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{_, Formatted2} = format_mfa_md({M, F, length(A)}), | |||||
{[{reason, 'function not exported'} | Md], | |||||
["call to undefined function ", Formatted2, | |||||
" from ", Formatted]}; | |||||
format_reason_md({'function not exported', [{M, F, A, _Props},MFA|_]}) -> | |||||
%% R15 line numbers | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{_, Formatted2} = format_mfa_md({M, F, length(A)}), | |||||
{[{reason, 'function not exported'} | Md], | |||||
["call to undefined function ", Formatted2, | |||||
" from ", Formatted]}; | |||||
format_reason_md({undef, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, undef} | Md], | |||||
["call to undefined function ", Formatted]}; | |||||
format_reason_md({bad_return, {_MFA, {'EXIT', Reason}}}) -> | |||||
format_reason_md(Reason); | |||||
format_reason_md({bad_return, {MFA, Val}}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, bad_return} | Md], | |||||
["bad return value ", print_val(Val), " from ", Formatted]}; | |||||
format_reason_md({bad_return_value, Val}) -> | |||||
{[{reason, bad_return}], | |||||
["bad return value: ", print_val(Val)]}; | |||||
format_reason_md({{bad_return_value, Val}, MFA}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, bad_return_value} | Md], | |||||
["bad return value: ", print_val(Val), " in ", Formatted]}; | |||||
format_reason_md({{badrecord, Record}, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, badrecord} | Md], | |||||
["bad record ", print_val(Record), " in ", Formatted]}; | |||||
format_reason_md({{case_clause, Val}, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, case_clause} | Md], | |||||
["no case clause matching ", print_val(Val), " in ", Formatted]}; | |||||
format_reason_md({function_clause, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, function_clause} | Md], | |||||
["no function clause matching ", Formatted]}; | |||||
format_reason_md({if_clause, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, if_clause} | Md], | |||||
["no true branch found while evaluating if expression in ", Formatted]}; | |||||
format_reason_md({{try_clause, Val}, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, try_clause} | Md], | |||||
["no try clause matching ", print_val(Val), " in ", Formatted]}; | |||||
format_reason_md({badarith, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, badarith} | Md], | |||||
["bad arithmetic expression in ", Formatted]}; | |||||
format_reason_md({{badmatch, Val}, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, badmatch} | Md], | |||||
["no match of right hand value ", print_val(Val), " in ", Formatted]}; | |||||
format_reason_md({emfile, _Trace}) -> | |||||
{[{reason, emfile}], | |||||
"maximum number of file descriptors exhausted, check ulimit -n"}; | |||||
format_reason_md({system_limit, [{M, F, _}|_] = Trace}) -> | |||||
Limit = case {M, F} of | |||||
{erlang, open_port} -> | |||||
"maximum number of ports exceeded"; | |||||
{erlang, spawn} -> | |||||
"maximum number of processes exceeded"; | |||||
{erlang, spawn_opt} -> | |||||
"maximum number of processes exceeded"; | |||||
{erlang, list_to_atom} -> | |||||
"tried to create an atom larger than 255, or maximum atom count exceeded"; | |||||
{ets, new} -> | |||||
"maximum number of ETS tables exceeded"; | |||||
_ -> | |||||
{Str, _} = lager_trunc_io:print(Trace, 500), | |||||
Str | |||||
end, | |||||
{[{reason, system_limit}], ["system limit: ", Limit]}; | |||||
format_reason_md({badarg, [MFA,MFA2|_]}) -> | |||||
case MFA of | |||||
{_M, _F, A, _Props} when is_list(A) -> | |||||
%% R15 line numbers | |||||
{Md, Formatted} = format_mfa_md(MFA2), | |||||
{_, Formatted2} = format_mfa_md(MFA), | |||||
{[{reason, badarg} | Md], | |||||
["bad argument in call to ", Formatted2, " in ", Formatted]}; | |||||
{_M, _F, A} when is_list(A) -> | |||||
{Md, Formatted} = format_mfa_md(MFA2), | |||||
{_, Formatted2} = format_mfa_md(MFA), | |||||
{[{reason, badarg} | Md], | |||||
["bad argument in call to ", Formatted2, " in ", Formatted]}; | |||||
_ -> | |||||
%% seems to be generated by a bad call to a BIF | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, badarg} | Md], | |||||
["bad argument in ", Formatted]} | |||||
end; | |||||
format_reason_md({{badarg, Stack}, _}) -> | |||||
format_reason_md({badarg, Stack}); | |||||
format_reason_md({{badarity, {Fun, Args}}, [MFA|_]}) -> | |||||
{arity, Arity} = lists:keyfind(arity, 1, erlang:fun_info(Fun)), | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, badarity} | Md], | |||||
[io_lib:format("fun called with wrong arity of ~w instead of ~w in ", | |||||
[length(Args), Arity]), Formatted]}; | |||||
format_reason_md({noproc, MFA}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, noproc} | Md], | |||||
["no such process or port in call to ", Formatted]}; | |||||
format_reason_md({{badfun, Term}, [MFA|_]}) -> | |||||
{Md, Formatted} = format_mfa_md(MFA), | |||||
{[{reason, badfun} | Md], | |||||
["bad function ", print_val(Term), " in ", Formatted]}; | |||||
format_reason_md({Reason, [{M, F, A}|_]}) when is_atom(M), is_atom(F), is_integer(A) -> | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
{_, Formatted2} = format_mfa_md({M, F, A}), | |||||
{Md, [Formatted, " in ", Formatted2]}; | |||||
format_reason_md({Reason, [{M, F, A, Props}|_]}) when is_atom(M), is_atom(F), is_integer(A), is_list(Props) -> | |||||
%% line numbers | |||||
{Md, Formatted} = format_reason_md(Reason), | |||||
{_, Formatted2} = format_mfa_md({M, F, A, Props}), | |||||
{Md, [Formatted, " in ", Formatted2]}; | |||||
format_reason_md(Reason) -> | |||||
{Str, _} = lager_trunc_io:print(Reason, 500), | |||||
{[], Str}. | |||||
%% backwards compatability shim | |||||
format_mfa(MFA) -> | |||||
element(2, format_mfa_md(MFA)). | |||||
-spec format_mfa_md(any()) -> {[{atom(), any()}], list()}. | |||||
format_mfa_md({M, F, A}) when is_list(A) -> | |||||
{FmtStr, Args} = format_args(A, [], []), | |||||
{[{module, M}, {function, F}], io_lib:format("~w:~w("++FmtStr++")", [M, F | Args])}; | |||||
format_mfa_md({M, F, A}) when is_integer(A) -> | |||||
{[{module, M}, {function, F}], io_lib:format("~w:~w/~w", [M, F, A])}; | |||||
format_mfa_md({M, F, A, Props}) when is_list(Props) -> | |||||
case get_value(line, Props) of | |||||
undefined -> | |||||
format_mfa_md({M, F, A}); | |||||
Line -> | |||||
{Md, Formatted} = format_mfa_md({M, F, A}), | |||||
{[{line, Line} | Md], [Formatted, io_lib:format(" line ~w", [Line])]} | |||||
end; | |||||
format_mfa_md([{M, F, A}| _]) -> | |||||
%% this kind of weird stacktrace can be generated by a uncaught throw in a gen_server | |||||
format_mfa_md({M, F, A}); | |||||
format_mfa_md([{M, F, A, Props}| _]) when is_list(Props) -> | |||||
%% this kind of weird stacktrace can be generated by a uncaught throw in a gen_server | |||||
%% TODO we might not always want to print the first MFA we see here, often it is more helpful | |||||
%% to print a lower one, but it is hard to programatically decide. | |||||
format_mfa_md({M, F, A, Props}); | |||||
format_mfa_md(Other) -> | |||||
{[], io_lib:format("~w", [Other])}. | |||||
format_args([], FmtAcc, ArgsAcc) -> | |||||
{string:join(lists:reverse(FmtAcc), ", "), lists:reverse(ArgsAcc)}; | |||||
format_args([H|T], FmtAcc, ArgsAcc) -> | |||||
{Str, _} = lager_trunc_io:print(H, 100), | |||||
format_args(T, ["~s"|FmtAcc], [Str|ArgsAcc]). | |||||
print_silly_list(L) when is_list(L) -> | |||||
case lager_stdlib:string_p(L) of | |||||
true -> | |||||
lager_trunc_io:format("~s", [L], ?DEFAULT_TRUNCATION); | |||||
_ -> | |||||
print_silly_list(L, [], []) | |||||
end; | |||||
print_silly_list(L) -> | |||||
{Str, _} = lager_trunc_io:print(L, ?DEFAULT_TRUNCATION), | |||||
Str. | |||||
print_silly_list([], Fmt, Acc) -> | |||||
lager_trunc_io:format(string:join(lists:reverse(Fmt), ", "), | |||||
lists:reverse(Acc), ?DEFAULT_TRUNCATION); | |||||
print_silly_list([{K,V}|T], Fmt, Acc) -> | |||||
print_silly_list(T, ["~p: ~p" | Fmt], [V, K | Acc]); | |||||
print_silly_list([H|T], Fmt, Acc) -> | |||||
print_silly_list(T, ["~p" | Fmt], [H | Acc]). | |||||
print_val(Val) -> | |||||
{Str, _} = lager_trunc_io:print(Val, 500), | |||||
Str. | |||||
%% @doc Faster than proplists, but with the same API as long as you don't need to | |||||
%% handle bare atom keys | |||||
get_value(Key, Value) -> | |||||
get_value(Key, Value, undefined). | |||||
get_value(Key, List, Default) -> | |||||
case lists:keyfind(Key, 1, List) of | |||||
false -> Default; | |||||
{Key, Value} -> Value | |||||
end. | |||||
supervisor_name({local, Name}) -> Name; | |||||
supervisor_name(Name) -> Name. | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
no_silent_hwm_drops_test_() -> | |||||
{timeout, 10000, | |||||
[ | |||||
fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_test_backend, warning}]), | |||||
application:set_env(lager, error_logger_redirect, true), | |||||
application:set_env(lager, error_logger_hwm, 5), | |||||
application:set_env(lager, error_logger_flush_queue, false), | |||||
application:set_env(lager, suppress_supervisor_start_stop, true), | |||||
application:set_env(lager, suppress_application_start_stop, true), | |||||
application:unset_env(lager, crash_log), | |||||
lager:start(), | |||||
try | |||||
{_, _, MS} = os:timestamp(), | |||||
timer:sleep((1000000 - MS) div 1000 + 1), | |||||
% start close to the beginning of a new second | |||||
[error_logger:error_msg("Foo ~p~n", [K]) || K <- lists:seq(1, 15)], | |||||
wait_for_message("lager_error_logger_h dropped 10 messages in the last second that exceeded the limit of 5 messages/sec", 100, 50), | |||||
% and once again | |||||
[error_logger:error_msg("Foo1 ~p~n", [K]) || K <- lists:seq(1, 20)], | |||||
wait_for_message("lager_error_logger_h dropped 15 messages in the last second that exceeded the limit of 5 messages/sec", 100, 50) | |||||
after | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end | |||||
end | |||||
] | |||||
}. | |||||
shaper_does_not_forward_sup_progress_messages_to_info_level_backend_test_() -> | |||||
{timeout, 10000, | |||||
[fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_test_backend, info}]), | |||||
application:set_env(lager, error_logger_redirect, true), | |||||
application:set_env(lager, error_logger_hwm, 5), | |||||
application:set_env(lager, suppress_supervisor_start_stop, false), | |||||
application:set_env(lager, suppress_application_start_stop, false), | |||||
application:unset_env(lager, crash_log), | |||||
lager:start(), | |||||
try | |||||
PidPlaceholder = self(), | |||||
SupervisorMsg = | |||||
[{supervisor, {PidPlaceholder,rabbit_connection_sup}}, | |||||
{started, | |||||
[{pid, PidPlaceholder}, | |||||
{name,helper_sup}, | |||||
{mfargs, | |||||
{rabbit_connection_helper_sup,start_link,[]}}, | |||||
{restart_type,intrinsic}, | |||||
{shutdown,infinity}, | |||||
{child_type,supervisor}]}], | |||||
ApplicationExit = | |||||
[{application, error_logger_lager_h_test}, | |||||
{exited, stopped}, | |||||
{type, permanent}], | |||||
error_logger:info_report("This is not a progress message"), | |||||
error_logger:info_report(ApplicationExit), | |||||
[error_logger:info_report(progress, SupervisorMsg) || _K <- lists:seq(0, 100)], | |||||
error_logger:info_report("This is not a progress message 2"), | |||||
% Note: this gets logged in slow environments: | |||||
% Application lager started on node nonode@nohost | |||||
wait_for_count(fun lager_test_backend:count/0, [3, 4], 100, 50), | |||||
% Note: this debug msg gets ignored in slow environments: | |||||
% Lager installed handler lager_test_backend into lager_event | |||||
wait_for_count(fun lager_test_backend:count_ignored/0, [0, 1], 100, 50) | |||||
after | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end | |||||
end | |||||
] | |||||
}. | |||||
supressed_messages_are_not_counted_for_hwm_test_() -> | |||||
{timeout, 10000, | |||||
[fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_test_backend, debug}]), | |||||
application:set_env(lager, error_logger_redirect, true), | |||||
application:set_env(lager, error_logger_hwm, 5), | |||||
application:set_env(lager, suppress_supervisor_start_stop, true), | |||||
application:set_env(lager, suppress_application_start_stop, true), | |||||
application:unset_env(lager, crash_log), | |||||
lager:start(), | |||||
try | |||||
PidPlaceholder = self(), | |||||
SupervisorMsg = | |||||
[{supervisor, {PidPlaceholder,rabbit_connection_sup}}, | |||||
{started, | |||||
[{pid, PidPlaceholder}, | |||||
{name,helper_sup}, | |||||
{mfargs, | |||||
{rabbit_connection_helper_sup,start_link,[]}}, | |||||
{restart_type,intrinsic}, | |||||
{shutdown,infinity}, | |||||
{child_type,supervisor}]}], | |||||
ApplicationExit = | |||||
[{application, error_logger_lager_h_test}, | |||||
{exited, stopped}, | |||||
{type, permanent}], | |||||
lager_test_backend:flush(), | |||||
error_logger:info_report("This is not a progress message"), | |||||
[error_logger:info_report(ApplicationExit) || _K <- lists:seq(0, 100)], | |||||
[error_logger:info_report(progress, SupervisorMsg) || _K <- lists:seq(0, 100)], | |||||
error_logger:info_report("This is not a progress message 2"), | |||||
wait_for_count(fun lager_test_backend:count/0, 2, 100, 50), | |||||
wait_for_count(fun lager_test_backend:count_ignored/0, 0, 100, 50) | |||||
after | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end | |||||
end | |||||
] | |||||
}. | |||||
wait_for_message(Expected, Tries, Sleep) -> | |||||
maybe_find_expected_message(lager_test_backend:get_buffer(), Expected, Tries, Sleep). | |||||
maybe_find_expected_message(_Buffer, Expected, 0, _Sleep) -> | |||||
throw({not_found, Expected}); | |||||
maybe_find_expected_message([], Expected, Tries, Sleep) -> | |||||
timer:sleep(Sleep), | |||||
maybe_find_expected_message(lager_test_backend:get_buffer(), Expected, Tries - 1, Sleep); | |||||
maybe_find_expected_message([{_Severity, _Date, Msg, _Metadata}|T], Expected, Tries, Sleep) -> | |||||
case lists:flatten(Msg) of | |||||
Expected -> | |||||
ok; | |||||
_ -> | |||||
maybe_find_expected_message(T, Expected, Tries, Sleep) | |||||
end. | |||||
wait_for_count(Fun, _Expected, 0, _Sleep) -> | |||||
Actual = Fun(), | |||||
Msg = io_lib:format("wait_for_count: fun ~p final value: ~p~n", [Fun, Actual]), | |||||
throw({failed, Msg}); | |||||
wait_for_count(Fun, Expected, Tries, Sleep) when is_list(Expected) -> | |||||
Actual = Fun(), | |||||
case lists:member(Actual, Expected) of | |||||
true -> | |||||
ok; | |||||
false -> | |||||
timer:sleep(Sleep), | |||||
wait_for_count(Fun, Expected, Tries - 1, Sleep) | |||||
end; | |||||
wait_for_count(Fun, Expected, Tries, Sleep) -> | |||||
case Fun() of | |||||
Expected -> | |||||
ok; | |||||
_ -> | |||||
timer:sleep(Sleep), | |||||
wait_for_count(Fun, Expected, Tries - 1, Sleep) | |||||
end. | |||||
-endif. |
@ -0,0 +1,64 @@ | |||||
%% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- | |||||
%% ex: ts=4 sw=4 et | |||||
{application, lager, | |||||
[ | |||||
{description, "Erlang logging framework"}, | |||||
{vsn, "3.8.1"}, | |||||
{modules, []}, | |||||
{applications, [ | |||||
kernel, | |||||
stdlib, | |||||
goldrush | |||||
]}, | |||||
{registered, [lager_sup, lager_event, lager_crash_log, lager_handler_watcher_sup]}, | |||||
{mod, {lager_app, []}}, | |||||
{env, [ | |||||
%% Note: application:start(lager) overwrites previously defined environment variables | |||||
%% thus declaration of default handlers is done at lager_app.erl | |||||
%% What colors to use with what log levels | |||||
{colored, false}, | |||||
{colors, [ | |||||
{debug, "\e[0;38m" }, | |||||
{info, "\e[1;37m" }, | |||||
{notice, "\e[1;36m" }, | |||||
{warning, "\e[1;33m" }, | |||||
{error, "\e[1;31m" }, | |||||
{critical, "\e[1;35m" }, | |||||
{alert, "\e[1;44m" }, | |||||
{emergency, "\e[1;41m" } | |||||
]}, | |||||
%% Whether to write a crash log, and where. False means no crash logger. | |||||
{crash_log, "log/crash.log"}, | |||||
%% Maximum size in bytes of events in the crash log - defaults to 65536 | |||||
{crash_log_msg_size, 65536}, | |||||
%% Maximum size of the crash log in bytes, before its rotated, set | |||||
%% to 0 to disable rotation - default is 0 | |||||
{crash_log_size, 10485760}, | |||||
%% What time to rotate the crash log - default is no time | |||||
%% rotation. See the README for a description of this format. | |||||
{crash_log_date, "$D0"}, | |||||
%% Number of rotated crash logs to keep, 0 means keep only the | |||||
%% current one - default is 0 | |||||
{crash_log_count, 5}, | |||||
%% Crash Log Rotator Module - default is lager_rotator_default | |||||
{crash_log_rotator, lager_rotator_default}, | |||||
%% Whether to redirect error_logger messages into the default lager_event sink - defaults to true | |||||
{error_logger_redirect, true}, | |||||
%% How many messages per second to allow from error_logger before we start dropping them | |||||
{error_logger_hwm, 50}, | |||||
%% How big the gen_event mailbox can get before it is | |||||
%% switched into sync mode. This value only applies to | |||||
%% the default sink; extra sinks can supply their own. | |||||
{async_threshold, 20}, | |||||
%% Switch back to async mode, when gen_event mailbox size | |||||
%% decrease from `async_threshold' to async_threshold - | |||||
%% async_threshold_window. This value only applies to the | |||||
%% default sink; extra sinks can supply their own. | |||||
{async_threshold_window, 5} | |||||
]}, | |||||
{licenses, ["Apache 2"]}, | |||||
{links, [{"Github", "https://github.com/erlang-lager/lager"}]} | |||||
]}. |
@ -0,0 +1,746 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 The lager logging framework. | |||||
-module(lager). | |||||
-include("lager.hrl"). | |||||
-define(LAGER_MD_KEY, '__lager_metadata'). | |||||
-define(TRACE_SINK, '__trace_sink'). | |||||
-define(ROTATE_TIMEOUT, 100000). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
%% API | |||||
-export([start/0, | |||||
log/3, log/4, log/5, | |||||
log_unsafe/4, | |||||
md/0, md/1, | |||||
rotate_handler/1, rotate_handler/2, rotate_sink/1, rotate_all/0, | |||||
trace/2, trace/3, trace_file/2, trace_file/3, trace_file/4, trace_console/1, trace_console/2, | |||||
install_trace/2, install_trace/3, remove_trace/1, trace_state/3, trace_func/3, | |||||
list_all_sinks/0, clear_all_traces/0, clear_trace_by_destination/1, stop_trace/1, stop_trace/3, status/0, | |||||
get_loglevel/1, get_loglevel/2, set_loglevel/2, set_loglevel/3, set_loglevel/4, get_loglevels/1, | |||||
update_loglevel_config/1, posix_error/1, set_loghwm/2, set_loghwm/3, set_loghwm/4, | |||||
safe_format/3, safe_format_chop/3, unsafe_format/2, dispatch_log/5, dispatch_log/7, dispatch_log/9, | |||||
do_log/9, do_log/10, do_log_unsafe/10, pr/2, pr/3, pr_stacktrace/1, pr_stacktrace/2]). | |||||
-type log_level() :: none | debug | info | notice | warning | error | critical | alert | emergency. | |||||
-type log_level_number() :: 0..7. | |||||
-export_type([log_level/0, log_level_number/0]). | |||||
-record(trace_func_state_v1, { | |||||
pid :: undefined | pid(), | |||||
level :: log_level(), | |||||
count :: infinity | pos_integer(), | |||||
format_string :: string(), | |||||
timeout :: infinity | pos_integer(), | |||||
started = os:timestamp() :: erlang:timestamp() %% use os:timestamp for compatability | |||||
}). | |||||
%% API | |||||
%% @doc installs a lager trace handler into the target process (using sys:install) at the specified level. | |||||
-spec install_trace(pid(), log_level()) -> ok. | |||||
install_trace(Pid, Level) -> | |||||
install_trace(Pid, Level, []). | |||||
-spec install_trace(pid(), log_level(), [{count, infinity | pos_integer()} | {format_string, string()} | {timeout, timeout()}]) -> ok. | |||||
install_trace(Pid, Level, Options) -> | |||||
sys:install(Pid, {fun ?MODULE:trace_func/3, trace_state(Pid, Level, Options)}). | |||||
%% @doc remove a previously installed lager trace handler from the target process. | |||||
-spec remove_trace(pid()) -> ok. | |||||
remove_trace(Pid) -> | |||||
sys:remove(Pid, fun ?MODULE:trace_func/3). | |||||
%% @doc Start the application. Mainly useful for using `-s lager' as a command | |||||
%% line switch to the VM to make lager start on boot. | |||||
start() -> start(lager). | |||||
start(App) -> | |||||
start_ok(App, application:start(App, permanent)). | |||||
start_ok(_App, ok) -> ok; | |||||
start_ok(_App, {error, {already_started, _App}}) -> ok; | |||||
start_ok(App, {error, {not_started, Dep}}) -> | |||||
ok = start(Dep), | |||||
start(App); | |||||
start_ok(App, {error, Reason}) -> | |||||
erlang:error({app_start_failed, App, Reason}). | |||||
%% @doc Get lager metadata for current process | |||||
-spec md() -> [{atom(), any()}]. | |||||
md() -> | |||||
case erlang:get(?LAGER_MD_KEY) of | |||||
undefined -> []; | |||||
MD -> MD | |||||
end. | |||||
%% @doc Set lager metadata for current process. | |||||
%% Will badarg if you don't supply a list of {key, value} tuples keyed by atoms. | |||||
-spec md([{atom(), any()},...]) -> ok. | |||||
md(NewMD) when is_list(NewMD) -> | |||||
%% make sure its actually a real proplist | |||||
case lists:all( | |||||
fun({Key, _Value}) when is_atom(Key) -> true; | |||||
(_) -> false | |||||
end, NewMD) of | |||||
true -> | |||||
erlang:put(?LAGER_MD_KEY, NewMD), | |||||
ok; | |||||
false -> | |||||
erlang:error(badarg) | |||||
end; | |||||
md(_) -> | |||||
erlang:error(badarg). | |||||
-spec dispatch_log(atom(), log_level(), list(), string(), list() | none, pos_integer(), safe | unsafe) -> ok | {error, lager_not_running} | {error, {sink_not_configured, atom()}}. | |||||
%% this is the same check that the parse transform bakes into the module at compile time | |||||
%% see lager_transform (lines 173-216) | |||||
dispatch_log(Sink, Severity, Metadata, Format, Args, Size, Safety) when is_atom(Severity)-> | |||||
SeverityAsInt=lager_util:level_to_num(Severity), | |||||
case {whereis(Sink), whereis(?DEFAULT_SINK), lager_config:get({Sink, loglevel}, {?LOG_NONE, []})} of | |||||
{undefined, undefined, _} -> {error, lager_not_running}; | |||||
{undefined, _LagerEventPid0, _} -> {error, {sink_not_configured, Sink}}; | |||||
{SinkPid, _LagerEventPid1, {Level, Traces}} when Safety =:= safe andalso ( (Level band SeverityAsInt) /= 0 orelse Traces /= [] ) -> | |||||
do_log(Severity, Metadata, Format, Args, Size, SeverityAsInt, Level, Traces, Sink, SinkPid); | |||||
{SinkPid, _LagerEventPid1, {Level, Traces}} when Safety =:= unsafe andalso ( (Level band SeverityAsInt) /= 0 orelse Traces /= [] ) -> | |||||
do_log_unsafe(Severity, Metadata, Format, Args, Size, SeverityAsInt, Level, Traces, Sink, SinkPid); | |||||
_ -> ok | |||||
end. | |||||
%% @private Should only be called externally from code generated from the parse transform | |||||
do_log(Severity, Metadata, Format, Args, Size, SeverityAsInt, LevelThreshold, TraceFilters, Sink, SinkPid) when is_atom(Severity) -> | |||||
FormatFun = fun() -> safe_format_chop(Format, Args, Size) end, | |||||
do_log_impl(Severity, Metadata, Format, Args, SeverityAsInt, LevelThreshold, TraceFilters, Sink, SinkPid, FormatFun). | |||||
do_log_impl(Severity, Metadata, Format, Args, SeverityAsInt, LevelThreshold, TraceFilters, Sink, SinkPid, FormatFun) -> | |||||
{Destinations, TraceSinkPid} = case TraceFilters of | |||||
[] -> | |||||
{[], undefined}; | |||||
_ -> | |||||
{lager_util:check_traces(Metadata,SeverityAsInt,TraceFilters,[]), whereis(?TRACE_SINK)} | |||||
end, | |||||
case (LevelThreshold band SeverityAsInt) /= 0 orelse Destinations /= [] of | |||||
true -> | |||||
Msg = case Args of | |||||
A when is_list(A) -> | |||||
FormatFun(); | |||||
_ -> | |||||
Format | |||||
end, | |||||
LagerMsg = lager_msg:new(Msg, | |||||
Severity, Metadata, Destinations), | |||||
case lager_config:get({Sink, async}, false) of | |||||
true -> | |||||
gen_event:notify(SinkPid, {log, LagerMsg}); | |||||
false -> | |||||
gen_event:sync_notify(SinkPid, {log, LagerMsg}) | |||||
end, | |||||
case TraceSinkPid /= undefined of | |||||
true -> | |||||
gen_event:notify(TraceSinkPid, {log, LagerMsg}); | |||||
false -> | |||||
ok | |||||
end; | |||||
false -> | |||||
ok | |||||
end. | |||||
%% @private Should only be called externally from code generated from the parse transform | |||||
%% Specifically, it would be level ++ `_unsafe' as in `info_unsafe'. | |||||
do_log_unsafe(Severity, Metadata, Format, Args, _Size, SeverityAsInt, LevelThreshold, TraceFilters, Sink, SinkPid) when is_atom(Severity) -> | |||||
FormatFun = fun() -> unsafe_format(Format, Args) end, | |||||
do_log_impl(Severity, Metadata, Format, Args, SeverityAsInt, LevelThreshold, TraceFilters, Sink, SinkPid, FormatFun). | |||||
%% backwards compatible with beams compiled with lager 1.x | |||||
dispatch_log(Severity, _Module, _Function, _Line, _Pid, Metadata, Format, Args, Size) -> | |||||
dispatch_log(Severity, Metadata, Format, Args, Size). | |||||
%% backwards compatible with beams compiled with lager 2.x | |||||
dispatch_log(Severity, Metadata, Format, Args, Size) -> | |||||
dispatch_log(?DEFAULT_SINK, Severity, Metadata, Format, Args, Size, safe). | |||||
%% backwards compatible with beams compiled with lager 2.x | |||||
do_log(Severity, Metadata, Format, Args, Size, SeverityAsInt, LevelThreshold, TraceFilters, SinkPid) -> | |||||
do_log(Severity, Metadata, Format, Args, Size, SeverityAsInt, | |||||
LevelThreshold, TraceFilters, ?DEFAULT_SINK, SinkPid). | |||||
%% TODO: | |||||
%% Consider making log2/4 that takes the Level, Pid and Message params of log/3 | |||||
%% along with a Sink param?? | |||||
%% @doc Manually log a message into lager without using the parse transform. | |||||
-spec log(log_level(), pid() | atom() | [tuple(),...], list()) -> ok | {error, lager_not_running}. | |||||
log(Level, Pid, Message) when is_pid(Pid); is_atom(Pid) -> | |||||
dispatch_log(Level, [{pid,Pid}], Message, [], ?DEFAULT_TRUNCATION); | |||||
log(Level, Metadata, Message) when is_list(Metadata) -> | |||||
dispatch_log(Level, Metadata, Message, [], ?DEFAULT_TRUNCATION). | |||||
%% @doc Manually log a message into lager without using the parse transform. | |||||
-spec log(log_level(), pid() | atom() | [tuple(),...], string(), list()) -> ok | {error, lager_not_running}. | |||||
log(Level, Pid, Format, Args) when is_pid(Pid); is_atom(Pid) -> | |||||
dispatch_log(Level, [{pid,Pid}], Format, Args, ?DEFAULT_TRUNCATION); | |||||
log(Level, Metadata, Format, Args) when is_list(Metadata) -> | |||||
dispatch_log(Level, Metadata, Format, Args, ?DEFAULT_TRUNCATION). | |||||
log_unsafe(Level, Metadata, Format, Args) when is_list(Metadata) -> | |||||
dispatch_log(?DEFAULT_SINK, Level, Metadata, Format, Args, ?DEFAULT_TRUNCATION, unsafe). | |||||
%% @doc Manually log a message into lager without using the parse transform. | |||||
-spec log(atom(), log_level(), pid() | atom() | [tuple(),...], string(), list()) -> ok | {error, lager_not_running}. | |||||
log(Sink, Level, Pid, Format, Args) when is_pid(Pid); is_atom(Pid) -> | |||||
dispatch_log(Sink, Level, [{pid,Pid}], Format, Args, ?DEFAULT_TRUNCATION, safe); | |||||
log(Sink, Level, Metadata, Format, Args) when is_list(Metadata) -> | |||||
dispatch_log(Sink, Level, Metadata, Format, Args, ?DEFAULT_TRUNCATION, safe). | |||||
validate_trace_filters(Filters, Level, Backend) -> | |||||
Sink = proplists:get_value(sink, Filters, ?DEFAULT_SINK), | |||||
{Sink, | |||||
lager_util:validate_trace({ | |||||
proplists:delete(sink, Filters), | |||||
Level, | |||||
Backend | |||||
}) | |||||
}. | |||||
trace_file(File, Filter) -> | |||||
trace_file(File, Filter, debug, []). | |||||
trace_file(File, Filter, Level) when is_atom(Level) -> | |||||
trace_file(File, Filter, Level, []); | |||||
trace_file(File, Filter, Options) when is_list(Options) -> | |||||
trace_file(File, Filter, debug, Options). | |||||
trace_file(File, Filter, Level, Options) -> | |||||
FileName = lager_util:expand_path(File), | |||||
case validate_trace_filters(Filter, Level, {lager_file_backend, FileName}) of | |||||
{Sink, {ok, Trace}} -> | |||||
Handlers = lager_config:global_get(handlers, []), | |||||
%% check if this file backend is already installed | |||||
Res = case lager_util:find_file(FileName, Handlers) of | |||||
false -> | |||||
%% install the handler | |||||
LogFileConfig = | |||||
lists:keystore(level, 1, | |||||
lists:keystore(file, 1, | |||||
Options, | |||||
{file, FileName}), | |||||
{level, none}), | |||||
HandlerInfo = | |||||
lager_app:start_handler(Sink, {lager_file_backend, FileName}, | |||||
LogFileConfig), | |||||
lager_config:global_set(handlers, [HandlerInfo|Handlers]), | |||||
{ok, installed}; | |||||
{_Watcher, _Handler, Sink} -> | |||||
{ok, exists}; | |||||
{_Watcher, _Handler, _OtherSink} -> | |||||
{error, file_in_use} | |||||
end, | |||||
case Res of | |||||
{ok, _} -> | |||||
add_trace_to_loglevel_config(Trace, Sink), | |||||
{ok, {{lager_file_backend, FileName}, Filter, Level}}; | |||||
{error, _} = E -> | |||||
E | |||||
end; | |||||
{_Sink, Error} -> | |||||
Error | |||||
end. | |||||
trace_console(Filter) -> | |||||
trace_console(Filter, debug). | |||||
trace_console(Filter, Level) -> | |||||
trace(lager_console_backend, Filter, Level). | |||||
trace(Backend, Filter) -> | |||||
trace(Backend, Filter, debug). | |||||
trace({lager_file_backend, File}, Filter, Level) -> | |||||
trace_file(File, Filter, Level); | |||||
trace(Backend, Filter, Level) -> | |||||
case validate_trace_filters(Filter, Level, Backend) of | |||||
{Sink, {ok, Trace}} -> | |||||
add_trace_to_loglevel_config(Trace, Sink), | |||||
{ok, {Backend, Filter, Level}}; | |||||
{_Sink, Error} -> | |||||
Error | |||||
end. | |||||
stop_trace(Backend, Filter, Level) -> | |||||
case validate_trace_filters(Filter, Level, Backend) of | |||||
{Sink, {ok, Trace}} -> | |||||
stop_trace_int(Trace, Sink); | |||||
{_Sink, Error} -> | |||||
Error | |||||
end. | |||||
stop_trace({Backend, Filter, Level}) -> | |||||
stop_trace(Backend, Filter, Level). | |||||
%% Important: validate_trace_filters orders the arguments of | |||||
%% trace tuples differently than the way outside callers have | |||||
%% the trace tuple. | |||||
%% | |||||
%% That is to say, outside they are represented as | |||||
%% `{Backend, Filter, Level}' | |||||
%% | |||||
%% and when they come back from validation, they're | |||||
%% `{Filter, Level, Backend}' | |||||
stop_trace_int({_Filter, _Level, Backend} = Trace, Sink) -> | |||||
{Level, Traces} = lager_config:get({Sink, loglevel}), | |||||
NewTraces = lists:delete(Trace, Traces), | |||||
_ = lager_util:trace_filter([ element(1, T) || T <- NewTraces ]), | |||||
%MinLevel = minimum_loglevel(get_loglevels() ++ get_trace_levels(NewTraces)), | |||||
lager_config:set({Sink, loglevel}, {Level, NewTraces}), | |||||
case get_loglevel(Sink, Backend) of | |||||
none -> | |||||
%% check no other traces point here | |||||
case lists:keyfind(Backend, 3, NewTraces) of | |||||
false -> | |||||
gen_event:delete_handler(Sink, Backend, []), | |||||
lager_config:global_set(handlers, | |||||
lists:keydelete(Backend, 1, | |||||
lager_config:global_get(handlers))); | |||||
_ -> | |||||
ok | |||||
end; | |||||
_ -> | |||||
ok | |||||
end, | |||||
ok. | |||||
list_all_sinks() -> | |||||
sets:to_list( | |||||
lists:foldl(fun({_Watcher, _Handler, Sink}, Set) -> | |||||
sets:add_element(Sink, Set) | |||||
end, | |||||
sets:new(), | |||||
lager_config:global_get(handlers, []))). | |||||
clear_traces_by_sink(Sinks) -> | |||||
lists:foreach(fun(S) -> | |||||
{Level, _Traces} = | |||||
lager_config:get({S, loglevel}), | |||||
lager_config:set({S, loglevel}, | |||||
{Level, []}) | |||||
end, | |||||
Sinks). | |||||
clear_trace_by_destination(ID) -> | |||||
Sinks = lists:sort(list_all_sinks()), | |||||
Traces = find_traces(Sinks), | |||||
[ stop_trace_int({Filter, Level, Destination}, Sink) || {Sink, {Filter, Level, Destination}} <- Traces, Destination == ID]. | |||||
clear_all_traces() -> | |||||
Handlers = lager_config:global_get(handlers, []), | |||||
clear_traces_by_sink(list_all_sinks()), | |||||
_ = lager_util:trace_filter(none), | |||||
lager_config:global_set(handlers, | |||||
lists:filter( | |||||
fun({Handler, _Watcher, Sink}) -> | |||||
case get_loglevel(Sink, Handler) of | |||||
none -> | |||||
gen_event:delete_handler(Sink, Handler, []), | |||||
false; | |||||
_ -> | |||||
true | |||||
end | |||||
end, Handlers)). | |||||
find_traces(Sinks) -> | |||||
lists:foldl(fun(S, Acc) -> | |||||
{_Level, Traces} = lager_config:get({S, loglevel}), | |||||
Acc ++ lists:map(fun(T) -> {S, T} end, Traces) | |||||
end, | |||||
[], | |||||
Sinks). | |||||
status() -> | |||||
Handlers = lager_config:global_get(handlers, []), | |||||
Sinks = lists:sort(list_all_sinks()), | |||||
Traces = find_traces(Sinks), | |||||
TraceCount = case length(Traces) of | |||||
0 -> 1; | |||||
N -> N | |||||
end, | |||||
Status = ["Lager status:\n", | |||||
[begin | |||||
Level = get_loglevel(Sink, Handler), | |||||
get_sink_handler_status(Sink, Handler, Level) | |||||
end || {Handler, _Watcher, Sink} <- lists:sort(fun({_, _, S1}, | |||||
{_, _, S2}) -> S1 =< S2 end, | |||||
Handlers)], | |||||
"Active Traces:\n", | |||||
[begin | |||||
LevelName = case Level of | |||||
{mask, Mask} -> | |||||
case lager_util:mask_to_levels(Mask) of | |||||
[] -> none; | |||||
Levels -> hd(Levels) | |||||
end; | |||||
Num -> | |||||
lager_util:num_to_level(Num) | |||||
end, | |||||
io_lib:format("Tracing messages matching ~p (sink ~s) at level ~p to ~p\n", | |||||
[Filter, Sink, LevelName, Destination]) | |||||
end || {Sink, {Filter, Level, Destination}} <- Traces], | |||||
[ | |||||
"Tracing Reductions:\n", | |||||
case ?DEFAULT_TRACER:info('query') of | |||||
{null, false} -> ""; | |||||
Query -> io_lib:format("~p~n", [Query]) | |||||
end | |||||
], | |||||
[ | |||||
"Tracing Statistics:\n ", | |||||
[ begin | |||||
[" ", atom_to_list(Table), ": ", | |||||
integer_to_list(?DEFAULT_TRACER:info(Table) div TraceCount), | |||||
"\n"] | |||||
end || Table <- [input, output, filter] ] | |||||
]], | |||||
io:put_chars(Status). | |||||
get_sink_handler_status(Sink, Handler, Level) -> | |||||
case Handler of | |||||
{lager_file_backend, File} -> | |||||
io_lib:format("File ~ts (~s) at level ~p\n", [File, Sink, Level]); | |||||
lager_console_backend -> | |||||
io_lib:format("Console (~s) at level ~p\n", [Sink, Level]); | |||||
_ -> | |||||
[] | |||||
end. | |||||
%% @doc Set the loglevel for a particular backend. | |||||
set_loglevel(Handler, Level) when is_atom(Level) -> | |||||
set_loglevel(?DEFAULT_SINK, Handler, undefined, Level). | |||||
%% @doc Set the loglevel for a particular backend that has multiple identifiers | |||||
%% (eg. the file backend). | |||||
set_loglevel(Handler, Ident, Level) when is_atom(Level) -> | |||||
set_loglevel(?DEFAULT_SINK, Handler, Ident, Level). | |||||
%% @doc Set the loglevel for a particular sink's backend that potentially has | |||||
%% multiple identifiers. (Use `undefined' if it doesn't have any.) | |||||
set_loglevel(Sink, Handler, Ident, Level) when is_atom(Level) -> | |||||
HandlerArg = case Ident of | |||||
undefined -> Handler; | |||||
_ -> {Handler, Ident} | |||||
end, | |||||
Reply = gen_event:call(Sink, HandlerArg, {set_loglevel, Level}, infinity), | |||||
update_loglevel_config(Sink), | |||||
Reply. | |||||
%% @doc Get the loglevel for a particular backend on the default sink. In the case that the backend | |||||
%% has multiple identifiers, the lowest is returned. | |||||
get_loglevel(Handler) -> | |||||
get_loglevel(?DEFAULT_SINK, Handler). | |||||
%% @doc Get the loglevel for a particular sink's backend. In the case that the backend | |||||
%% has multiple identifiers, the lowest is returned. | |||||
get_loglevel(Sink, Handler) -> | |||||
case gen_event:call(Sink, Handler, get_loglevel, infinity) of | |||||
{mask, Mask} -> | |||||
case lager_util:mask_to_levels(Mask) of | |||||
[] -> none; | |||||
Levels -> hd(Levels) | |||||
end; | |||||
X when is_integer(X) -> | |||||
lager_util:num_to_level(X); | |||||
Y -> Y | |||||
end. | |||||
%% @doc Try to convert an atom to a posix error, but fall back on printing the | |||||
%% term if its not a valid posix error code. | |||||
posix_error(Error) when is_atom(Error) -> | |||||
case erl_posix_msg:message(Error) of | |||||
"unknown POSIX error" -> atom_to_list(Error); | |||||
Message -> Message | |||||
end; | |||||
posix_error(Error) -> | |||||
safe_format_chop("~p", [Error], ?DEFAULT_TRUNCATION). | |||||
%% @private | |||||
get_loglevels(Sink) -> | |||||
[gen_event:call(Sink, Handler, get_loglevel, infinity) || | |||||
Handler <- gen_event:which_handlers(Sink)]. | |||||
%% @doc Set the loghwm for the default sink. | |||||
set_loghwm(Handler, Hwm) when is_integer(Hwm) -> | |||||
set_loghwm(?DEFAULT_SINK, Handler, Hwm). | |||||
%% @doc Set the loghwm for a particular backend. | |||||
set_loghwm(Sink, Handler, Hwm) when is_integer(Hwm) -> | |||||
gen_event:call(Sink, Handler, {set_loghwm, Hwm}, infinity). | |||||
%% @doc Set the loghwm (log high water mark) for file backends with multiple identifiers | |||||
set_loghwm(Sink, Handler, Ident, Hwm) when is_integer(Hwm) -> | |||||
gen_event:call(Sink, {Handler, Ident}, {set_loghwm, Hwm}, infinity). | |||||
%% @private | |||||
add_trace_to_loglevel_config(Trace, Sink) -> | |||||
{MinLevel, Traces} = lager_config:get({Sink, loglevel}), | |||||
case lists:member(Trace, Traces) of | |||||
false -> | |||||
NewTraces = [Trace|Traces], | |||||
_ = lager_util:trace_filter([ element(1, T) || T <- NewTraces]), | |||||
lager_config:set({Sink, loglevel}, {MinLevel, [Trace|Traces]}); | |||||
_ -> | |||||
ok | |||||
end. | |||||
%% @doc recalculate min log level | |||||
update_loglevel_config(error_logger) -> | |||||
%% Not a sink under our control, part of the Erlang logging | |||||
%% utility that error_logger_lager_h attaches to | |||||
true; | |||||
update_loglevel_config(Sink) -> | |||||
{_, Traces} = lager_config:get({Sink, loglevel}, {ignore_me, []}), | |||||
MinLog = minimum_loglevel(get_loglevels(Sink)), | |||||
lager_config:set({Sink, loglevel}, {MinLog, Traces}). | |||||
%% @private | |||||
minimum_loglevel(Levels) -> | |||||
lists:foldl(fun({mask, Mask}, Acc) -> | |||||
Mask bor Acc; | |||||
(Level, Acc) when is_integer(Level) -> | |||||
{mask, Mask} = lager_util:config_to_mask(lager_util:num_to_level(Level)), | |||||
Mask bor Acc; | |||||
(_, Acc) -> | |||||
Acc | |||||
end, 0, Levels). | |||||
%% @doc Print the format string `Fmt' with `Args' safely with a size | |||||
%% limit of `Limit'. If the format string is invalid, or not enough | |||||
%% arguments are supplied 'FORMAT ERROR' is printed with the offending | |||||
%% arguments. The caller is NOT crashed. | |||||
safe_format(Fmt, Args, Limit) -> | |||||
safe_format(Fmt, Args, Limit, []). | |||||
safe_format(Fmt, Args, Limit, Options) -> | |||||
try lager_trunc_io:format(Fmt, Args, Limit, Options) | |||||
catch | |||||
_:_ -> lager_trunc_io:format("FORMAT ERROR: ~p ~p", [Fmt, Args], Limit) | |||||
end. | |||||
%% @private | |||||
safe_format_chop(Fmt, Args, Limit) -> | |||||
safe_format(Fmt, Args, Limit, [{chomp, true}]). | |||||
%% @private Print the format string `Fmt' with `Args' without a size limit. | |||||
%% This is unsafe because the output of this function is unbounded. | |||||
%% | |||||
%% Log messages with unbounded size will kill your application dead as | |||||
%% OTP mechanisms stuggle to cope with them. So this function is | |||||
%% intended <b>only</b> for messages which have a reasonable bounded | |||||
%% size before they're formatted. | |||||
%% | |||||
%% If the format string is invalid or not enough arguments are | |||||
%% supplied a 'FORMAT ERROR' message is printed instead with the | |||||
%% offending arguments. The caller is NOT crashed. | |||||
unsafe_format(Fmt, Args) -> | |||||
try io_lib:format(Fmt, Args) | |||||
catch | |||||
_:_ -> io_lib:format("FORMAT ERROR: ~p ~p", [Fmt, Args]) | |||||
end. | |||||
%% @doc Print a record or a list of records lager found during parse transform | |||||
pr(Record, Module) when is_tuple(Record), is_atom(element(1, Record)) -> | |||||
pr(Record, Module, []); | |||||
pr(List, Module) when is_list(List) -> | |||||
pr(List, Module, []); | |||||
pr(Record, _) -> | |||||
Record. | |||||
%% @doc Print a record or a list of records lager found during parse transform | |||||
pr(Record, Module, Options) when is_tuple(Record), is_atom(element(1, Record)), is_list(Options) -> | |||||
try | |||||
case is_record_known(Record, Module) of | |||||
false -> | |||||
Record; | |||||
{RecordName, RecordFields} -> | |||||
{'$lager_record', RecordName, | |||||
zip(RecordFields, tl(tuple_to_list(Record)), Module, Options, [])} | |||||
end | |||||
catch | |||||
error:undef -> | |||||
Record | |||||
end; | |||||
pr([Head|Tail], Module, Options) when is_list(Options) -> | |||||
[pr(Head, Module, Options)|pr(Tail, Module, Options)]; | |||||
pr(Record, _, _) -> | |||||
Record. | |||||
zip([FieldName|RecordFields], [FieldValue|Record], Module, Options, ToReturn) when is_list(FieldValue) -> | |||||
zip(RecordFields, Record, Module, Options, | |||||
[{FieldName, pr(FieldValue, Module, Options)}|ToReturn]); | |||||
zip([FieldName|RecordFields], [FieldValue|Record], Module, Options, ToReturn) -> | |||||
Compress = lists:member(compress, Options), | |||||
case is_tuple(FieldValue) andalso | |||||
tuple_size(FieldValue) > 0 andalso | |||||
is_atom(element(1, FieldValue)) andalso | |||||
is_record_known(FieldValue, Module) of | |||||
false when Compress andalso FieldValue =:= undefined -> | |||||
zip(RecordFields, Record, Module, Options, ToReturn); | |||||
false -> | |||||
zip(RecordFields, Record, Module, Options, [{FieldName, FieldValue}|ToReturn]); | |||||
_Else -> | |||||
F = {FieldName, pr(FieldValue, Module, Options)}, | |||||
zip(RecordFields, Record, Module, Options, [F|ToReturn]) | |||||
end; | |||||
zip([], [], _Module, _Compress, ToReturn) -> | |||||
lists:reverse(ToReturn). | |||||
is_record_known(Record, Module) -> | |||||
Name = element(1, Record), | |||||
Attrs = Module:module_info(attributes), | |||||
case lists:keyfind(lager_records, 1, Attrs) of | |||||
false -> false; | |||||
{lager_records, Records} -> | |||||
case lists:keyfind(Name, 1, Records) of | |||||
false -> false; | |||||
{Name, RecordFields} -> | |||||
case (tuple_size(Record) - 1) =:= length(RecordFields) of | |||||
false -> false; | |||||
true -> {Name, RecordFields} | |||||
end | |||||
end | |||||
end. | |||||
%% @doc Print stacktrace in human readable form | |||||
pr_stacktrace(Stacktrace) -> | |||||
Stacktrace1 = case application:get_env(lager, reverse_pretty_stacktrace, true) of | |||||
true -> | |||||
lists:reverse(Stacktrace); | |||||
_ -> | |||||
Stacktrace | |||||
end, | |||||
pr_stacktrace_(Stacktrace1). | |||||
pr_stacktrace_(Stacktrace) -> | |||||
Indent = "\n ", | |||||
lists:foldl( | |||||
fun(Entry, Acc) -> | |||||
Acc ++ Indent ++ error_logger_lager_h:format_mfa(Entry) | |||||
end, | |||||
[], | |||||
Stacktrace). | |||||
pr_stacktrace(Stacktrace, {Class, Reason}) -> | |||||
case application:get_env(lager, reverse_pretty_stacktrace, true) of | |||||
true -> | |||||
lists:flatten( | |||||
pr_stacktrace_(lists:reverse(Stacktrace)) ++ "\n" ++ io_lib:format("~s:~p", [Class, Reason])); | |||||
_ -> | |||||
lists:flatten( | |||||
io_lib:format("~s:~p", [Class, Reason]) ++ pr_stacktrace_(Stacktrace)) | |||||
end. | |||||
rotate_sink(Sink) -> | |||||
Handlers = lager_config:global_get(handlers), | |||||
RotateHandlers = lists:filtermap( | |||||
fun({Handler,_,S}) when S == Sink -> {true, {Handler, Sink}}; | |||||
(_) -> false | |||||
end, | |||||
Handlers), | |||||
rotate_handlers(RotateHandlers). | |||||
rotate_all() -> | |||||
rotate_handlers(lists:map(fun({H,_,S}) -> {H, S} end, | |||||
lager_config:global_get(handlers))). | |||||
rotate_handlers(Handlers) -> | |||||
[ rotate_handler(Handler, Sink) || {Handler, Sink} <- Handlers ]. | |||||
rotate_handler(Handler) -> | |||||
Handlers = lager_config:global_get(handlers), | |||||
case lists:keyfind(Handler, 1, Handlers) of | |||||
{Handler, _, Sink} -> rotate_handler(Handler, Sink); | |||||
false -> ok | |||||
end. | |||||
rotate_handler(Handler, Sink) -> | |||||
gen_event:call(Sink, Handler, rotate, ?ROTATE_TIMEOUT). | |||||
%% @private | |||||
trace_func(#trace_func_state_v1{pid=Pid, level=Level, format_string=Fmt}=FuncState, Event, ProcState) -> | |||||
_ = lager:log(Level, Pid, Fmt, [Event, ProcState]), | |||||
check_timeout(decrement_count(FuncState)). | |||||
%% @private | |||||
trace_state(Pid, Level, Options) -> | |||||
#trace_func_state_v1{pid=Pid, | |||||
level=Level, | |||||
count=proplists:get_value(count, Options, infinity), | |||||
timeout=proplists:get_value(timeout, Options, infinity), | |||||
format_string=proplists:get_value(format_string, Options, "TRACE ~p ~p")}. | |||||
decrement_count(#trace_func_state_v1{count=infinity} = FuncState) -> | |||||
FuncState; | |||||
decrement_count(#trace_func_state_v1{count=1}) -> | |||||
%% hit the counter limit | |||||
done; | |||||
decrement_count(#trace_func_state_v1{count=Count} = FuncState) -> | |||||
FuncState#trace_func_state_v1{count=Count - 1}. | |||||
check_timeout(#trace_func_state_v1{timeout=infinity} = FuncState) -> | |||||
FuncState; | |||||
check_timeout(#trace_func_state_v1{timeout=Timeout, started=Started} = FuncState) -> | |||||
case (timer:now_diff(os:timestamp(), Started) / 1000) > Timeout of | |||||
true -> | |||||
done; | |||||
false -> | |||||
FuncState | |||||
end. | |||||
-ifdef(TEST). | |||||
get_sink_handler_status_ascii_test() -> | |||||
File = "C:\\ProgramData\\Directory With Spaces\\lager.log", | |||||
validate_status(File). | |||||
get_sink_handler_status_latin_test() -> | |||||
File = "C:\\ProgramData\\Tést Directory\\lager.log", | |||||
validate_status(File). | |||||
get_sink_handler_status_unicode_test() -> | |||||
File = "C:\\ProgramData\\찦차를 타고 온 펲시맨과 쑛다리 똠방각하 (Korean)\\lager.log", | |||||
validate_status(File). | |||||
validate_status(File) -> | |||||
Handler = {lager_file_backend, File}, | |||||
Status = get_sink_handler_status(?DEFAULT_SINK, Handler, debug), | |||||
?assertNotEqual(nomatch, string:find(Status, File)). | |||||
-endif. |
@ -0,0 +1,426 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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's application module. Not a lot to see here. | |||||
%% @private | |||||
-module(lager_app). | |||||
-behaviour(application). | |||||
-include("lager.hrl"). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
-export([start/0, | |||||
start/2, | |||||
start_handler/3, | |||||
configure_sink/2, | |||||
stop/1, | |||||
boot/1]). | |||||
%% The `application:get_env/3` compatibility wrapper was useful | |||||
%% for other modules in r15 and before | |||||
-export([get_env/3]). | |||||
-define(FILENAMES, '__lager_file_backend_filenames'). | |||||
-define(THROTTLE, lager_backend_throttle). | |||||
-define(DEFAULT_HANDLER_CONF, | |||||
[{lager_console_backend, [{level, info}]}, | |||||
{lager_file_backend, | |||||
[{file, "log/error.log"}, {level, error}, | |||||
{size, 10485760}, {date, "$D0"}, {count, 5}] | |||||
}, | |||||
{lager_file_backend, | |||||
[{file, "log/console.log"}, {level, info}, | |||||
{size, 10485760}, {date, "$D0"}, {count, 5}] | |||||
} | |||||
]). | |||||
start() -> | |||||
application:start(lager). | |||||
start_throttle(Sink, Threshold, Window) -> | |||||
_ = supervisor:start_child(lager_handler_watcher_sup, | |||||
[Sink, ?THROTTLE, [Threshold, Window]]), | |||||
ok. | |||||
determine_async_behavior(_Sink, undefined, _Window) -> | |||||
ok; | |||||
determine_async_behavior(_Sink, Threshold, _Window) when not is_integer(Threshold) orelse Threshold < 0 -> | |||||
error_logger:error_msg("Invalid value for 'async_threshold': ~p~n", | |||||
[Threshold]), | |||||
throw({error, bad_config}); | |||||
determine_async_behavior(Sink, Threshold, undefined) -> | |||||
start_throttle(Sink, Threshold, erlang:trunc(Threshold * 0.2)); | |||||
determine_async_behavior(_Sink, Threshold, Window) when not is_integer(Window) orelse Window > Threshold orelse Window < 0 -> | |||||
error_logger:error_msg( | |||||
"Invalid value for 'async_threshold_window': ~p~n", [Window]), | |||||
throw({error, bad_config}); | |||||
determine_async_behavior(Sink, Threshold, Window) -> | |||||
start_throttle(Sink, Threshold, Window). | |||||
start_handlers(_Sink, undefined) -> | |||||
ok; | |||||
start_handlers(_Sink, Handlers) when not is_list(Handlers) -> | |||||
error_logger:error_msg( | |||||
"Invalid value for 'handlers' (must be list): ~p~n", [Handlers]), | |||||
throw({error, bad_config}); | |||||
start_handlers(Sink, Handlers) -> | |||||
%% handlers failing to start are handled in the handler_watcher | |||||
lager_config:global_set(handlers, | |||||
lager_config:global_get(handlers, []) ++ | |||||
lists:map(fun({Module, Config}) -> | |||||
check_handler_config(Module, Config), | |||||
start_handler(Sink, Module, Config); | |||||
(_) -> | |||||
throw({error, bad_config}) | |||||
end, | |||||
expand_handlers(Handlers))), | |||||
ok. | |||||
start_handler(Sink, Module, Config) -> | |||||
{ok, Watcher} = supervisor:start_child(lager_handler_watcher_sup, | |||||
[Sink, Module, Config]), | |||||
{Module, Watcher, Sink}. | |||||
check_handler_config({lager_file_backend, F}, Config) when is_list(Config); is_tuple(Config) -> | |||||
Fs = case get(?FILENAMES) of | |||||
undefined -> ordsets:new(); | |||||
X -> X | |||||
end, | |||||
case ordsets:is_element(F, Fs) of | |||||
true -> | |||||
error_logger:error_msg( | |||||
"Cannot have same file (~p) in multiple file backends~n", [F]), | |||||
throw({error, bad_config}); | |||||
false -> | |||||
put(?FILENAMES, | |||||
ordsets:add_element(F, Fs)) | |||||
end, | |||||
ok; | |||||
check_handler_config(_Handler, Config) when is_list(Config) orelse is_atom(Config) -> | |||||
ok; | |||||
check_handler_config(Handler, _BadConfig) -> | |||||
throw({error, {bad_config, Handler}}). | |||||
clean_up_config_checks() -> | |||||
erase(?FILENAMES). | |||||
interpret_hwm(undefined) -> | |||||
undefined; | |||||
interpret_hwm(HWM) when not is_integer(HWM) orelse HWM < 0 -> | |||||
_ = lager:log(warning, self(), "Invalid error_logger high water mark: ~p, disabling", [HWM]), | |||||
undefined; | |||||
interpret_hwm(HWM) -> | |||||
HWM. | |||||
maybe_install_sink_killer(_Sink, undefined, _ReinstallTimer) -> ok; | |||||
maybe_install_sink_killer(Sink, HWM, undefined) -> maybe_install_sink_killer(Sink, HWM, 5000); | |||||
maybe_install_sink_killer(Sink, HWM, ReinstallTimer) when is_integer(HWM) andalso is_integer(ReinstallTimer) | |||||
andalso HWM >= 0 andalso ReinstallTimer >= 0 -> | |||||
_ = supervisor:start_child(lager_handler_watcher_sup, [Sink, lager_manager_killer, | |||||
[HWM, ReinstallTimer]]); | |||||
maybe_install_sink_killer(_Sink, HWM, ReinstallTimer) -> | |||||
error_logger:error_msg("Invalid value for 'killer_hwm': ~p or 'killer_reinstall_after': ~p", [HWM, ReinstallTimer]), | |||||
throw({error, bad_config}). | |||||
-spec start_error_logger_handler(boolean(), pos_integer(), list()) -> list(). | |||||
start_error_logger_handler(false, _HWM, _Whitelist) -> | |||||
[]; | |||||
start_error_logger_handler(true, HWM, WhiteList) -> | |||||
GlStrategy = case application:get_env(lager, error_logger_groupleader_strategy) of | |||||
undefined -> | |||||
handle; | |||||
{ok, GlStrategy0} when | |||||
GlStrategy0 =:= handle; | |||||
GlStrategy0 =:= ignore; | |||||
GlStrategy0 =:= mirror -> | |||||
GlStrategy0; | |||||
{ok, BadGlStrategy} -> | |||||
error_logger:error_msg( | |||||
"Invalid value for 'error_logger_groupleader_strategy': ~p~n", | |||||
[BadGlStrategy]), | |||||
throw({error, bad_config}) | |||||
end, | |||||
case whereis(error_logger) of | |||||
undefined -> | |||||
%% On OTP 21 and above, error_logger is deprecated in favor of 'logger' | |||||
%% As a band-aid, boot up error_logger anyway and install it as a logger handler | |||||
%% we can't use error_logger:add_report_handler because we want supervision of the handler | |||||
%% so we have to manually add the logger handler | |||||
%% | |||||
%% Longer term we should be installing a logger handler instead, but this will bridge the gap | |||||
%% for now. | |||||
_ = error_logger:start(), | |||||
_ = logger:add_handler(error_logger,error_logger,#{level=>info,filter_default=>log}), | |||||
ok = maybe_remove_logger_handler(); | |||||
_ -> | |||||
ok | |||||
end, | |||||
%% capture which handlers we removed from error_logger so we can restore them when lager stops | |||||
OldHandlers = case supervisor:start_child(lager_handler_watcher_sup, [error_logger, error_logger_lager_h, [HWM, GlStrategy]]) of | |||||
{ok, _} -> | |||||
[begin error_logger:delete_report_handler(X), X end || | |||||
X <- gen_event:which_handlers(error_logger) -- [error_logger_lager_h | WhiteList]]; | |||||
{error, _} -> | |||||
[] | |||||
end, | |||||
OldHandlers. | |||||
%% On OTP 21.1 and higher we need to remove the `default' handler. | |||||
%% But it might not exist, so we will wrap this in a try-catch | |||||
%% block | |||||
maybe_remove_logger_handler() -> | |||||
try | |||||
ok = logger:remove_handler(default) | |||||
catch | |||||
error:undef -> ok; | |||||
Err:Reason -> | |||||
error_logger:error_msg("calling logger:remove_handler(default) failed: ~p ~p", | |||||
[Err, Reason]) | |||||
end. | |||||
configure_sink(Sink, SinkDef) -> | |||||
lager_config:new_sink(Sink), | |||||
ChildId = lager_util:make_internal_sink_name(Sink), | |||||
_ = supervisor:start_child(lager_sup, | |||||
{ChildId, | |||||
{gen_event, start_link, | |||||
[{local, Sink}]}, | |||||
permanent, 5000, worker, dynamic}), | |||||
determine_async_behavior(Sink, proplists:get_value(async_threshold, SinkDef), | |||||
proplists:get_value(async_threshold_window, SinkDef) | |||||
), | |||||
_ = maybe_install_sink_killer(Sink, proplists:get_value(killer_hwm, SinkDef), | |||||
proplists:get_value(killer_reinstall_after, SinkDef)), | |||||
start_handlers(Sink, | |||||
proplists:get_value(handlers, SinkDef, [])), | |||||
lager:update_loglevel_config(Sink). | |||||
configure_extra_sinks(Sinks) -> | |||||
lists:foreach(fun({Sink, Proplist}) -> configure_sink(Sink, Proplist) end, | |||||
Sinks). | |||||
-spec get_env(atom(), atom(), term()) -> term(). | |||||
get_env(Application, Key, Default) -> | |||||
application:get_env(Application, Key, Default). | |||||
start(_StartType, _StartArgs) -> | |||||
{ok, Pid} = lager_sup:start_link(), | |||||
SavedHandlers = boot(), | |||||
_ = boot('__all_extra'), | |||||
_ = boot('__traces'), | |||||
clean_up_config_checks(), | |||||
{ok, Pid, SavedHandlers}. | |||||
boot() -> | |||||
%% Handle the default sink. | |||||
determine_async_behavior(?DEFAULT_SINK, | |||||
application:get_env(lager, async_threshold, undefined), | |||||
application:get_env(lager, async_threshold_window, undefined)), | |||||
_ = maybe_install_sink_killer(?DEFAULT_SINK, application:get_env(lager, killer_hwm, undefined), | |||||
application:get_env(lager, killer_reinstall_after, undefined)), | |||||
start_handlers(?DEFAULT_SINK, | |||||
application:get_env(lager, handlers, ?DEFAULT_HANDLER_CONF)), | |||||
lager:update_loglevel_config(?DEFAULT_SINK), | |||||
SavedHandlers = start_error_logger_handler( | |||||
application:get_env(lager, error_logger_redirect, true), | |||||
interpret_hwm(application:get_env(lager, error_logger_hwm, 0)), | |||||
application:get_env(lager, error_logger_whitelist, []) | |||||
), | |||||
SavedHandlers. | |||||
boot('__traces') -> | |||||
_ = lager_util:trace_filter(none), | |||||
ok = add_configured_traces(); | |||||
boot('__all_extra') -> | |||||
configure_extra_sinks(application:get_env(lager, extra_sinks, [])); | |||||
boot(?DEFAULT_SINK) -> boot(); | |||||
boot(Sink) -> | |||||
AllSinksDef = application:get_env(lager, extra_sinks, []), | |||||
boot_sink(Sink, lists:keyfind(Sink, 1, AllSinksDef)). | |||||
boot_sink(Sink, {Sink, Def}) -> | |||||
configure_sink(Sink, Def); | |||||
boot_sink(Sink, false) -> | |||||
configure_sink(Sink, []). | |||||
stop(Handlers) -> | |||||
lists:foreach(fun(Handler) -> | |||||
error_logger:add_report_handler(Handler) | |||||
end, Handlers), | |||||
lager_config:cleanup(). | |||||
expand_handlers([]) -> | |||||
[]; | |||||
expand_handlers([{lager_file_backend, [{Key, _Value}|_]=Config}|T]) when is_atom(Key) -> | |||||
%% this is definitely a new-style config, no expansion needed | |||||
[maybe_make_handler_id(lager_file_backend, Config) | expand_handlers(T)]; | |||||
expand_handlers([{lager_file_backend, Configs}|T]) -> | |||||
?INT_LOG(notice, "Deprecated lager_file_backend config detected, please consider updating it", []), | |||||
[ {lager_file_backend:config_to_id(Config), Config} || Config <- Configs] ++ | |||||
expand_handlers(T); | |||||
expand_handlers([{Mod, Config}|T]) when is_atom(Mod) -> | |||||
[maybe_make_handler_id(Mod, Config) | expand_handlers(T)]; | |||||
expand_handlers([H|T]) -> | |||||
[H | expand_handlers(T)]. | |||||
add_configured_traces() -> | |||||
Traces = case application:get_env(lager, traces) of | |||||
undefined -> | |||||
[]; | |||||
{ok, TraceVal} -> | |||||
TraceVal | |||||
end, | |||||
lists:foreach(fun start_configured_trace/1, Traces), | |||||
ok. | |||||
start_configured_trace({Handler, Filter}) -> | |||||
{ok, _} = lager:trace(Handler, Filter); | |||||
start_configured_trace({Handler, Filter, Level}) when is_atom(Level) -> | |||||
{ok, _} = lager:trace(Handler, Filter, Level). | |||||
maybe_make_handler_id(Mod, Config) -> | |||||
%% Allow the backend to generate a gen_event handler id, if it wants to. | |||||
%% We don't use erlang:function_exported here because that requires the module | |||||
%% already be loaded, which is unlikely at this phase of startup. Using code:load | |||||
%% caused undesirable side-effects with generating code-coverage reports. | |||||
try Mod:config_to_id(Config) of | |||||
Id -> | |||||
{Id, Config} | |||||
catch | |||||
error:undef -> | |||||
{Mod, Config} | |||||
end. | |||||
-ifdef(TEST). | |||||
application_config_mangling_test_() -> | |||||
[ | |||||
{"Explode the file backend handlers", | |||||
?_assertMatch( | |||||
[{lager_console_backend, [{level, info}]}, | |||||
{{lager_file_backend,"error.log"},{"error.log",error,10485760, "$D0",5}}, | |||||
{{lager_file_backend,"console.log"},{"console.log",info,10485760, "$D0",5}} | |||||
], | |||||
expand_handlers([{lager_console_backend, [{level, info}]}, | |||||
{lager_file_backend, [ | |||||
{"error.log", error, 10485760, "$D0", 5}, | |||||
{"console.log", info, 10485760, "$D0", 5} | |||||
]}] | |||||
)) | |||||
}, | |||||
{"Explode the short form of backend file handlers", | |||||
?_assertMatch( | |||||
[{lager_console_backend, [{level, info}]}, | |||||
{{lager_file_backend,"error.log"},{"error.log",error}}, | |||||
{{lager_file_backend,"console.log"},{"console.log",info}} | |||||
], | |||||
expand_handlers([{lager_console_backend, [{level, info}]}, | |||||
{lager_file_backend, [ | |||||
{"error.log", error}, | |||||
{"console.log", info} | |||||
]}] | |||||
)) | |||||
}, | |||||
{"Explode with formatter info", | |||||
?_assertMatch( | |||||
[{{lager_file_backend,"test.log"}, [{"test.log", debug, 10485760, "$D0", 5},{lager_default_formatter,["[",severity,"] ", message, "\n"]}]}, | |||||
{{lager_file_backend,"test2.log"}, [{"test2.log",debug, 10485760, "$D0", 5},{lager_default_formatter,["2>[",severity,"] ", message, "\n"]}]}], | |||||
expand_handlers([{lager_file_backend, [ | |||||
[{"test.log", debug, 10485760, "$D0", 5},{lager_default_formatter,["[",severity,"] ", message, "\n"]}], | |||||
[{"test2.log",debug, 10485760, "$D0", 5},{lager_default_formatter,["2>[",severity,"] ",message, "\n"]}] | |||||
] | |||||
}]) | |||||
) | |||||
}, | |||||
{"Explode short form with short formatter info", | |||||
?_assertMatch( | |||||
[{{lager_file_backend,"test.log"}, [{"test.log", debug},{lager_default_formatter,["[",severity,"] ", message, "\n"]}]}, | |||||
{{lager_file_backend,"test2.log"}, [{"test2.log",debug},{lager_default_formatter}]}], | |||||
expand_handlers([{lager_file_backend, [ | |||||
[{"test.log", debug},{lager_default_formatter,["[",severity,"] ", message, "\n"]}], | |||||
[{"test2.log",debug},{lager_default_formatter}] | |||||
] | |||||
}]) | |||||
) | |||||
}, | |||||
{"New form needs no expansion", | |||||
?_assertMatch([ | |||||
{{lager_file_backend,"test.log"}, [{file, "test.log"}]}, | |||||
{{lager_file_backend,"test2.log"}, [{file, "test2.log"}, {level, info}, {sync_on, none}]}, | |||||
{{lager_file_backend,"test3.log"}, [{formatter, lager_default_formatter}, {file, "test3.log"}]} | |||||
], | |||||
expand_handlers([ | |||||
{lager_file_backend, [{file, "test.log"}]}, | |||||
{lager_file_backend, [{file, "test2.log"}, {level, info}, {sync_on, none}]}, | |||||
{lager_file_backend, [{formatter, lager_default_formatter},{file, "test3.log"}]} | |||||
]) | |||||
) | |||||
} | |||||
]. | |||||
check_handler_config_test_() -> | |||||
Good = expand_handlers(?DEFAULT_HANDLER_CONF), | |||||
Bad = expand_handlers([{lager_console_backend, [{level, info}]}, | |||||
{lager_file_backend, [{file, "same_file.log"}]}, | |||||
{lager_file_backend, [{file, "same_file.log"}, {level, info}]}]), | |||||
AlsoBad = [{lager_logstash_backend, | |||||
{level, info}, | |||||
{output, {udp, "localhost", 5000}}, | |||||
{format, json}, | |||||
{json_encoder, jiffy}}], | |||||
BadToo = [{fail, {fail}}], | |||||
OldSchoolLagerGood = expand_handlers([{lager_console_backend, [{level, info}]}, | |||||
{lager_file_backend, [ | |||||
{"./log/error.log",error,10485760,"$D0",5}, | |||||
{"./log/console.log",info,10485760,"$D0",5}, | |||||
{"./log/debug.log",debug,10485760,"$D0",5} | |||||
]}]), | |||||
NewConfigMissingList = expand_handlers([{foo_backend, {file, "same_file.log"}}]), | |||||
[ | |||||
{"lager_file_backend_good", | |||||
?_assertEqual([ok, ok, ok], [ check_handler_config(M,C) || {M,C} <- Good ]) | |||||
}, | |||||
{"lager_file_backend_bad", | |||||
?_assertThrow({error, bad_config}, [ check_handler_config(M,C) || {M,C} <- Bad ]) | |||||
}, | |||||
{"Invalid config dies", | |||||
?_assertThrow({error, bad_config}, start_handlers(foo, AlsoBad)) | |||||
}, | |||||
{"Invalid config dies", | |||||
?_assertThrow({error, {bad_config, _}}, start_handlers(foo, BadToo)) | |||||
}, | |||||
{"Old Lager config works", | |||||
?_assertEqual([ok, ok, ok, ok], [ check_handler_config(M, C) || {M, C} <- OldSchoolLagerGood]) | |||||
}, | |||||
{"New Config missing its list should fail", | |||||
?_assertThrow({error, {bad_config, foo_backend}}, [ check_handler_config(M, C) || {M, C} <- NewConfigMissingList]) | |||||
} | |||||
]. | |||||
-endif. |
@ -0,0 +1,105 @@ | |||||
%% Copyright (c) 2011-2013 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 A simple gen_event backend used to monitor mailbox size and | |||||
%% switch log messages between synchronous and asynchronous modes. | |||||
%% A gen_event handler is used because a process getting its own mailbox | |||||
%% size doesn't involve getting a lock, and gen_event handlers run in their | |||||
%% parent's process. | |||||
-module(lager_backend_throttle). | |||||
-include("lager.hrl"). | |||||
-behaviour(gen_event). | |||||
-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, | |||||
code_change/3]). | |||||
%% | |||||
%% Allow test code to verify that we're doing the needful. | |||||
-ifdef(TEST). | |||||
-define(ETS_TABLE, async_threshold_test). | |||||
-define(TOGGLE_SYNC(), test_increment(sync_toggled)). | |||||
-define(TOGGLE_ASYNC(), test_increment(async_toggled)). | |||||
-else. | |||||
-define(TOGGLE_SYNC(), true). | |||||
-define(TOGGLE_ASYNC(), true). | |||||
-endif. | |||||
-record(state, { | |||||
sink :: atom(), | |||||
hwm :: non_neg_integer(), | |||||
window_min :: non_neg_integer(), | |||||
async = true :: boolean() | |||||
}). | |||||
init([{sink, Sink}, Hwm, Window]) -> | |||||
lager_config:set({Sink, async}, true), | |||||
{ok, #state{sink=Sink, hwm=Hwm, window_min=Hwm - Window}}. | |||||
handle_call(get_loglevel, State) -> | |||||
{ok, {mask, ?LOG_NONE}, State}; | |||||
handle_call({set_loglevel, _Level}, State) -> | |||||
{ok, ok, State}; | |||||
handle_call(_Request, State) -> | |||||
{ok, ok, State}. | |||||
handle_event({log, _Message},State) -> | |||||
{message_queue_len, Len} = erlang:process_info(self(), message_queue_len), | |||||
case {Len > State#state.hwm, Len < State#state.window_min, State#state.async} of | |||||
{true, _, true} -> | |||||
%% need to flip to sync mode | |||||
?TOGGLE_SYNC(), | |||||
lager_config:set({State#state.sink, async}, false), | |||||
{ok, State#state{async=false}}; | |||||
{_, true, false} -> | |||||
%% need to flip to async mode | |||||
?TOGGLE_ASYNC(), | |||||
lager_config:set({State#state.sink, async}, true), | |||||
{ok, State#state{async=true}}; | |||||
_ -> | |||||
%% nothing needs to change | |||||
{ok, State} | |||||
end; | |||||
handle_event(_Event, State) -> | |||||
{ok, State}. | |||||
handle_info(_Info, State) -> | |||||
{ok, State}. | |||||
%% @private | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
%% @private | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. | |||||
-ifdef(TEST). | |||||
test_get(Key) -> | |||||
get_default(ets:lookup(?ETS_TABLE, Key)). | |||||
test_increment(Key) -> | |||||
ets:insert(?ETS_TABLE, | |||||
{Key, test_get(Key) + 1}). | |||||
get_default([]) -> | |||||
0; | |||||
get_default([{_Key, Value}]) -> | |||||
Value. | |||||
-endif. |
@ -0,0 +1,121 @@ | |||||
-module(lager_common_test_backend). | |||||
-behavior(gen_event). | |||||
%% gen_event callbacks | |||||
-export([init/1, | |||||
handle_call/2, | |||||
handle_event/2, | |||||
handle_info/2, | |||||
terminate/2, | |||||
code_change/3]). | |||||
-export([get_logs/0, | |||||
bounce/0, | |||||
bounce/1]). | |||||
%% holds the log messages for retreival on terminate | |||||
-record(state, {level :: {mask, integer()}, | |||||
formatter :: atom(), | |||||
format_config :: any(), | |||||
log = [] :: list()}). | |||||
-include("lager.hrl"). | |||||
-define(TERSE_FORMAT,[time, " ", color, "[", severity,"] ", message]). | |||||
%% @doc Before every test, just | |||||
%% lager_common_test_backend:bounce(Level) with the log level of your | |||||
%% choice. Every message will be passed along to ct:pal for your | |||||
%% viewing in the common_test reports. Also, you can call | |||||
%% lager_common_test_backend:get_logs/0 to get a list of all log | |||||
%% messages this backend has received during your test. You can then | |||||
%% search that list for expected log messages. | |||||
-spec get_logs() -> [iolist()] | {error, term()}. | |||||
get_logs() -> | |||||
gen_event:call(lager_event, ?MODULE, get_logs, infinity). | |||||
bounce() -> | |||||
bounce(error). | |||||
bounce(Level) -> | |||||
_ = application:stop(lager), | |||||
application:set_env(lager, suppress_application_start_stop, true), | |||||
application:set_env(lager, handlers, | |||||
[ | |||||
{lager_common_test_backend, [Level, false]} | |||||
]), | |||||
ok = lager:start(), | |||||
%% we care more about getting all of our messages here than being | |||||
%% careful with the amount of memory that we're using. | |||||
error_logger_lager_h:set_high_water(100000), | |||||
ok. | |||||
-spec(init(integer()|atom()|[term()]) -> {ok, #state{}} | {error, atom()}). | |||||
%% @private | |||||
%% @doc Initializes the event handler | |||||
init([Level, true]) -> % for backwards compatibility | |||||
init([Level,{lager_default_formatter,[{eol, "\n"}]}]); | |||||
init([Level,false]) -> % for backwards compatibility | |||||
init([Level,{lager_default_formatter,?TERSE_FORMAT ++ ["\n"]}]); | |||||
init([Level,{Formatter,FormatterConfig}]) when is_atom(Formatter) -> | |||||
case lists:member(Level, ?LEVELS) of | |||||
true -> | |||||
{ok, #state{level=lager_util:config_to_mask(Level), | |||||
formatter=Formatter, | |||||
format_config=FormatterConfig}}; | |||||
_ -> | |||||
{error, bad_log_level} | |||||
end; | |||||
init(Level) -> | |||||
init([Level,{lager_default_formatter,?TERSE_FORMAT ++ ["\n"]}]). | |||||
-spec(handle_event(tuple(), #state{}) -> {ok, #state{}}). | |||||
%% @private | |||||
handle_event({log, Message}, | |||||
#state{level=L,formatter=Formatter,format_config=FormatConfig,log=Logs} = State) -> | |||||
case lager_util:is_loggable(Message,L,?MODULE) of | |||||
true -> | |||||
Log = Formatter:format(Message,FormatConfig), | |||||
ct:pal(Log), | |||||
{ok, State#state{log=[Log|Logs]}}; | |||||
false -> | |||||
{ok, State} | |||||
end; | |||||
handle_event(Event, State) -> | |||||
ct:pal(Event), | |||||
{ok, State#state{log = [Event|State#state.log]}}. | |||||
-spec(handle_call(any(), #state{}) -> {ok, any(), #state{}}). | |||||
%% @private | |||||
%% @doc gets and sets loglevel. This is part of the lager backend api. | |||||
handle_call(get_loglevel, #state{level=Level} = State) -> | |||||
{ok, Level, State}; | |||||
handle_call({set_loglevel, Level}, State) -> | |||||
case lists:member(Level, ?LEVELS) of | |||||
true -> | |||||
{ok, ok, State#state{level=lager_util:config_to_mask(Level)}}; | |||||
_ -> | |||||
{ok, {error, bad_log_level}, State} | |||||
end; | |||||
handle_call(get_logs, #state{log = Logs} = State) -> | |||||
{ok, lists:reverse(Logs), State}; | |||||
handle_call(_, State) -> | |||||
{ok, ok, State}. | |||||
-spec(handle_info(any(), #state{}) -> {ok, #state{}}). | |||||
%% @private | |||||
%% @doc gen_event callback, does nothing. | |||||
handle_info(_, State) -> | |||||
{ok, State}. | |||||
-spec(code_change(any(), #state{}, any()) -> {ok, #state{}}). | |||||
%% @private | |||||
%% @doc gen_event callback, does nothing. | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. | |||||
-spec(terminate(any(), #state{}) -> {ok, list()}). | |||||
%% @doc gen_event callback, does nothing. | |||||
terminate(_Reason, #state{log=Logs}) -> | |||||
{ok, lists:reverse(Logs)}. |
@ -0,0 +1,136 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 Helper functions for working with lager's runtime config | |||||
-module(lager_config). | |||||
-include("lager.hrl"). | |||||
-export([new/0, new_sink/1, get/1, get/2, set/2, | |||||
global_get/1, global_get/2, global_set/2, cleanup/0]). | |||||
-define(TBL, lager_config). | |||||
-define(GLOBAL, '_global'). | |||||
%% For multiple sinks, the key is now the registered event name and the old key | |||||
%% as a tuple. | |||||
%% | |||||
%% {{lager_event, loglevel}, Value} instead of {loglevel, Value} | |||||
new() -> | |||||
init(), | |||||
new_sink(?DEFAULT_SINK), | |||||
%% Need to be able to find the `lager_handler_watcher' for all handlers | |||||
insert_new({?GLOBAL, handlers}, []), | |||||
ok. | |||||
new_sink(Sink) -> | |||||
%% use insert_new here so that if we're in an appup we don't mess anything up | |||||
%% | |||||
%% until lager is completely started, allow all messages to go through | |||||
insert_new({Sink, loglevel}, {element(2, lager_util:config_to_mask(debug)), []}). | |||||
global_get(Key) -> | |||||
global_get(Key, undefined). | |||||
global_get(Key, Default) -> | |||||
get({?GLOBAL, Key}, Default). | |||||
global_set(Key, Value) -> | |||||
set({?GLOBAL, Key}, Value). | |||||
get({_Sink, _Key}=FullKey) -> | |||||
get(FullKey, undefined); | |||||
get(Key) -> | |||||
get({?DEFAULT_SINK, Key}, undefined). | |||||
get({Sink, Key}, Default) -> | |||||
lookup({Sink, Key}, Default); | |||||
get(Key, Default) -> | |||||
get({?DEFAULT_SINK, Key}, Default). | |||||
set({Sink, Key}, Value) -> | |||||
insert({Sink, Key}, Value); | |||||
set(Key, Value) -> | |||||
set({?DEFAULT_SINK, Key}, Value). | |||||
%% check if we can use persistent_term for config | |||||
%% persistent term was added in OTP 21.2 but we can't | |||||
%% check minor versions with macros so we're stuck waiting | |||||
%% for OTP 22 | |||||
-ifdef(HAVE_PERSISTENT_TERM). | |||||
init() -> | |||||
ok. | |||||
insert(Key, Value) -> | |||||
persistent_term:put({?TBL, Key}, Value). | |||||
insert_new(Key, Value) -> | |||||
try persistent_term:get({?TBL, Key}) of | |||||
_Value -> | |||||
false | |||||
catch error:badarg -> | |||||
insert(Key, Value), | |||||
true | |||||
end. | |||||
lookup(Key, Default) -> | |||||
try persistent_term:get({?TBL, Key}) of | |||||
Value -> Value | |||||
catch | |||||
error:badarg -> | |||||
Default | |||||
end. | |||||
cleanup() -> | |||||
[ persistent_term:erase(K) || {{?TBL, _} = K, _} <- persistent_term:get() ]. | |||||
-else. | |||||
init() -> | |||||
%% set up the ETS configuration table | |||||
_ = try ets:new(?TBL, [named_table, public, set, {keypos, 1}, {read_concurrency, true}]) of | |||||
_Result -> | |||||
ok | |||||
catch | |||||
error:badarg -> | |||||
?INT_LOG(warning, "Table ~p already exists", [?TBL]) | |||||
end. | |||||
insert(Key, Value) -> | |||||
ets:insert(?TBL, {Key, Value}). | |||||
insert_new(Key, Value) -> | |||||
ets:insert_new(?TBL, {Key, Value}). | |||||
lookup(Key, Default) -> | |||||
try | |||||
case ets:lookup(?TBL, Key) of | |||||
[] -> | |||||
Default; | |||||
[{Key, Res}] -> | |||||
Res | |||||
end | |||||
catch | |||||
_:_ -> | |||||
Default | |||||
end. | |||||
cleanup() -> ok. | |||||
-endif. | |||||
@ -0,0 +1,604 @@ | |||||
%% Copyright (c) 2011-2012, 2014 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 Console backend for lager. | |||||
%% Configuration is a proplist with the following keys: | |||||
%% <ul> | |||||
%% <li>`level' - log level to use</li> | |||||
%% <li>`use_stderr' - either `true' or `false', defaults to false. If set to true, | |||||
%% use standard error to output console log messages</li> | |||||
%% <li>`formatter' - the module to use when formatting log messages. Defaults to | |||||
%% `lager_default_formatter'</li> | |||||
%% <li>`formatter_config' - the format configuration string. Defaults to | |||||
%% `time [ severity ] message'</li> | |||||
%% </ul> | |||||
-module(lager_console_backend). | |||||
-behaviour(gen_event). | |||||
-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, | |||||
code_change/3]). | |||||
-record(state, {level :: {'mask', integer()}, | |||||
out = user :: user | standard_error | pid(), | |||||
id :: atom() | {atom(), any()}, | |||||
formatter :: atom(), | |||||
format_config :: any(), | |||||
colors=[] :: list()}). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-endif. | |||||
-include("lager.hrl"). | |||||
-define(TERSE_FORMAT,[time, " ", color, "[", severity,"] ", message]). | |||||
-define(DEFAULT_FORMAT_CONFIG, ?TERSE_FORMAT ++ [eol()]). | |||||
-define(FORMAT_CONFIG_OFF, [{eol, eol()}]). | |||||
-ifdef(TEST). | |||||
-define(DEPRECATED(_Msg), ok). | |||||
-else. | |||||
-define(DEPRECATED(Msg), | |||||
io:format(user, "WARNING: This is a deprecated console configuration. Please use \"~w\" instead.~n", [Msg])). | |||||
-endif. | |||||
%% @private | |||||
init([Level]) when is_atom(Level) -> | |||||
?DEPRECATED([{level, Level}]), | |||||
init([{level, Level}]); | |||||
init([Level, true]) when is_atom(Level) -> % for backwards compatibility | |||||
?DEPRECATED([{level, Level}, {formatter_config, [{eol, "\\r\\n\\"}]}]), | |||||
init([{level, Level}, {formatter_config, ?FORMAT_CONFIG_OFF}]); | |||||
init([Level, false]) when is_atom(Level) -> % for backwards compatibility | |||||
?DEPRECATED([{level, Level}]), | |||||
init([{level, Level}]); | |||||
init(Options) when is_list(Options) -> | |||||
true = validate_options(Options), | |||||
Colors = case application:get_env(lager, colored) of | |||||
{ok, true} -> | |||||
{ok, LagerColors} = application:get_env(lager, colors), | |||||
LagerColors; | |||||
_ -> [] | |||||
end, | |||||
Level = get_option(level, Options, undefined), | |||||
try {is_new_style_console_available(), lager_util:config_to_mask(Level)} of | |||||
{false, _} -> | |||||
Msg = "Lager's console backend is incompatible with the 'old' shell, not enabling it", | |||||
%% be as noisy as possible, log to every possible place | |||||
try | |||||
alarm_handler:set_alarm({?MODULE, "WARNING: " ++ Msg}) | |||||
catch | |||||
_:_ -> | |||||
error_logger:warning_msg(Msg ++ "~n") | |||||
end, | |||||
io:format("WARNING: " ++ Msg ++ "~n"), | |||||
?INT_LOG(warning, Msg, []), | |||||
{error, {fatal, old_shell}}; | |||||
{true, L} -> | |||||
[UseErr, GroupLeader, ID, Formatter, Config] = [ get_option(K, Options, Default) || {K, Default} <- [ | |||||
{use_stderr, false}, | |||||
{group_leader, false}, | |||||
{id, ?MODULE}, | |||||
{formatter, lager_default_formatter}, | |||||
{formatter_config, ?DEFAULT_FORMAT_CONFIG} | |||||
] | |||||
], | |||||
Out = case UseErr of | |||||
false -> | |||||
case GroupLeader of | |||||
false -> user; | |||||
GLPid when is_pid(GLPid) -> | |||||
erlang:monitor(process, GLPid), | |||||
GLPid | |||||
end; | |||||
true -> standard_error | |||||
end, | |||||
{ok, #state{level=L, | |||||
id=ID, | |||||
out=Out, | |||||
formatter=Formatter, | |||||
format_config=Config, | |||||
colors=Colors}} | |||||
catch | |||||
_:_ -> | |||||
{error, {fatal, bad_log_level}} | |||||
end; | |||||
init(Level) when is_atom(Level) -> | |||||
?DEPRECATED([{level, Level}]), | |||||
init([{level, Level}]); | |||||
init(Other) -> | |||||
{error, {fatal, {bad_console_config, Other}}}. | |||||
validate_options([]) -> true; | |||||
validate_options([{level, L}|T]) when is_atom(L) -> | |||||
case lists:member(L, ?LEVELS) of | |||||
false -> | |||||
throw({error, {fatal, {bad_level, L}}}); | |||||
true -> | |||||
validate_options(T) | |||||
end; | |||||
validate_options([{use_stderr, true}|T]) -> | |||||
validate_options(T); | |||||
validate_options([{use_stderr, false}|T]) -> | |||||
validate_options(T); | |||||
validate_options([{formatter, M}|T]) when is_atom(M) -> | |||||
validate_options(T); | |||||
validate_options([{formatter_config, C}|T]) when is_list(C) -> | |||||
validate_options(T); | |||||
validate_options([{group_leader, L}|T]) when is_pid(L) -> | |||||
validate_options(T); | |||||
validate_options([{id, {?MODULE, _}}|T]) -> | |||||
validate_options(T); | |||||
validate_options([H|_]) -> | |||||
throw({error, {fatal, {bad_console_config, H}}}). | |||||
get_option(K, Options, Default) -> | |||||
case lists:keyfind(K, 1, Options) of | |||||
{K, V} -> V; | |||||
false -> Default | |||||
end. | |||||
%% @private | |||||
handle_call(get_loglevel, #state{level=Level} = State) -> | |||||
{ok, Level, State}; | |||||
handle_call({set_loglevel, Level}, State) -> | |||||
try lager_util:config_to_mask(Level) of | |||||
Levels -> | |||||
{ok, ok, State#state{level=Levels}} | |||||
catch | |||||
_:_ -> | |||||
{ok, {error, bad_log_level}, State} | |||||
end; | |||||
handle_call(_Request, State) -> | |||||
{ok, ok, State}. | |||||
%% @private | |||||
handle_event({log, Message}, | |||||
#state{level=L,out=Out,formatter=Formatter,format_config=FormatConfig,colors=Colors,id=ID} = State) -> | |||||
case lager_util:is_loggable(Message, L, ID) of | |||||
true -> | |||||
io:put_chars(Out, Formatter:format(Message,FormatConfig,Colors)), | |||||
{ok, State}; | |||||
false -> | |||||
{ok, State} | |||||
end; | |||||
handle_event(_Event, State) -> | |||||
{ok, State}. | |||||
%% @private | |||||
handle_info({'DOWN', _, process, Out, _}, #state{out=Out}) -> | |||||
remove_handler; | |||||
handle_info(_Info, State) -> | |||||
{ok, State}. | |||||
%% @private | |||||
terminate(remove_handler, _State=#state{id=ID}) -> | |||||
%% have to do this asynchronously because we're in the event handlr | |||||
spawn(fun() -> lager:clear_trace_by_destination(ID) end), | |||||
ok; | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
%% @private | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. | |||||
eol() -> | |||||
case application:get_env(lager, colored) of | |||||
{ok, true} -> | |||||
"\e[0m\r\n"; | |||||
_ -> | |||||
"\r\n" | |||||
end. | |||||
-ifdef(TEST). | |||||
is_new_style_console_available() -> | |||||
true. | |||||
-else. | |||||
is_new_style_console_available() -> | |||||
%% Criteria: | |||||
%% 1. If the user has specified '-noshell' on the command line, | |||||
%% then we will pretend that the new-style console is available. | |||||
%% If there is no shell at all, then we don't have to worry | |||||
%% about log events being blocked by the old-style shell. | |||||
%% 2. Windows doesn't support the new shell, so all windows users | |||||
%% have is the oldshell. | |||||
%% 3. If the user_drv process is registered, all is OK. | |||||
%% 'user_drv' is a registered proc name used by the "new" | |||||
%% console driver. | |||||
init:get_argument(noshell) /= error orelse | |||||
element(1, os:type()) /= win32 orelse | |||||
is_pid(whereis(user_drv)). | |||||
-endif. | |||||
-ifdef(TEST). | |||||
console_config_validation_test_() -> | |||||
Good = [{level, info}, {use_stderr, true}], | |||||
Bad1 = [{level, foo}, {use_stderr, flase}], | |||||
Bad2 = [{level, info}, {use_stderr, flase}], | |||||
AllGood = [{level, info}, {formatter, my_formatter}, | |||||
{formatter_config, ["blort", "garbage"]}, | |||||
{use_stderr, false}], | |||||
[ | |||||
?_assertEqual(true, validate_options(Good)), | |||||
?_assertThrow({error, {fatal, {bad_level, foo}}}, validate_options(Bad1)), | |||||
?_assertThrow({error, {fatal, {bad_console_config, {use_stderr, flase}}}}, validate_options(Bad2)), | |||||
?_assertEqual(true, validate_options(AllGood)) | |||||
]. | |||||
console_log_test_() -> | |||||
%% tiny recursive fun that pretends to be a group leader | |||||
F = fun(Self) -> | |||||
fun() -> | |||||
YComb = fun(Fun) -> | |||||
receive | |||||
{io_request, From, ReplyAs, {put_chars, unicode, _Msg}} = Y -> | |||||
From ! {io_reply, ReplyAs, ok}, | |||||
Self ! Y, | |||||
Fun(Fun); | |||||
Other -> | |||||
?debugFmt("unexpected message ~p~n", [Other]), | |||||
Self ! Other | |||||
end | |||||
end, | |||||
YComb(YComb) | |||||
end | |||||
end, | |||||
{foreach, | |||||
fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, []), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
lager:start(), | |||||
whereis(user) | |||||
end, | |||||
fun(User) -> | |||||
unregister(user), | |||||
register(user, User), | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end, | |||||
[ | |||||
{"regular console logging", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [{level, info}]), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
lager:log(info, self(), "Test message"), | |||||
receive | |||||
{io_request, From, ReplyAs, {put_chars, unicode, Msg}} -> | |||||
From ! {io_reply, ReplyAs, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[info]", TestMsg], re:split(Msg, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
500 -> | |||||
?assert(false) | |||||
end | |||||
end | |||||
}, | |||||
{"verbose console logging", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [info, true]), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
lager:info("Test message"), | |||||
PidStr = pid_to_list(self()), | |||||
receive | |||||
{io_request, _, _, {put_chars, unicode, Msg}} -> | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, _, "[info]", PidStr, _, TestMsg], re:split(Msg, "[ @]", [{return, list}, {parts, 6}])) | |||||
after | |||||
500 -> | |||||
?assert(false) | |||||
end | |||||
end | |||||
}, | |||||
{"custom format console logging", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
gen_event:add_handler(lager_event, lager_console_backend, | |||||
[{level, info}, {formatter, lager_default_formatter}, {formatter_config, [date,"#",time,"#",severity,"#",node,"#",pid,"#", | |||||
module,"#",function,"#",file,"#",line,"#",message,"\r\n"]}]), | |||||
lager_config:set({lager_event, loglevel}, {?INFO, []}), | |||||
lager:info("Test message"), | |||||
PidStr = pid_to_list(self()), | |||||
NodeStr = atom_to_list(node()), | |||||
ModuleStr = atom_to_list(?MODULE), | |||||
receive | |||||
{io_request, _, _, {put_chars, unicode, Msg}} -> | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, _, "info", NodeStr, PidStr, ModuleStr, _, _, _, TestMsg], | |||||
re:split(Msg, "#", [{return, list}, {parts, 10}])) | |||||
after | |||||
500 -> | |||||
?assert(false) | |||||
end | |||||
end | |||||
}, | |||||
{"tracing should work", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [{level, info}]), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
lager:debug("Test message"), | |||||
receive | |||||
{io_request, From, ReplyAs, {put_chars, unicode, _Msg}} -> | |||||
From ! {io_reply, ReplyAs, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end, | |||||
{ok, _} = lager:trace_console([{module, ?MODULE}]), | |||||
lager:debug("Test message"), | |||||
receive | |||||
{io_request, From1, ReplyAs1, {put_chars, unicode, Msg1}} -> | |||||
From1 ! {io_reply, ReplyAs1, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[debug]", TestMsg], re:split(Msg1, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
500 -> | |||||
?assert(false) | |||||
end | |||||
end | |||||
}, | |||||
{"tracing doesn't duplicate messages", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [{level, info}]), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
lager:debug("Test message"), | |||||
receive | |||||
{io_request, From, ReplyAs, {put_chars, unicode, _Msg}} -> | |||||
From ! {io_reply, ReplyAs, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end, | |||||
{ok, _} = lager:trace_console([{module, ?MODULE}]), | |||||
lager:error("Test message"), | |||||
receive | |||||
{io_request, From1, ReplyAs1, {put_chars, unicode, Msg1}} -> | |||||
From1 ! {io_reply, ReplyAs1, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[error]", TestMsg], re:split(Msg1, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
1000 -> | |||||
?assert(false) | |||||
end, | |||||
%% make sure this event wasn't duplicated | |||||
receive | |||||
{io_request, From2, ReplyAs2, {put_chars, unicode, _Msg2}} -> | |||||
From2 ! {io_reply, ReplyAs2, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end | |||||
end | |||||
}, | |||||
{"blacklisting a loglevel works", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [{level, info}]), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
lager:set_loglevel(lager_console_backend, '!=info'), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
lager:debug("Test message"), | |||||
receive | |||||
{io_request, From1, ReplyAs1, {put_chars, unicode, Msg1}} -> | |||||
From1 ! {io_reply, ReplyAs1, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[debug]", TestMsg], re:split(Msg1, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
1000 -> | |||||
?assert(false) | |||||
end, | |||||
%% info is blacklisted | |||||
lager:info("Test message"), | |||||
receive | |||||
{io_request, From2, ReplyAs2, {put_chars, unicode, _Msg2}} -> | |||||
From2 ! {io_reply, ReplyAs2, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end | |||||
end | |||||
}, | |||||
{"whitelisting a loglevel works", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
unregister(user), | |||||
register(user, Pid), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [{level, info}]), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
lager:set_loglevel(lager_console_backend, '=debug'), | |||||
erlang:group_leader(Pid, whereis(lager_event)), | |||||
lager:debug("Test message"), | |||||
receive | |||||
{io_request, From1, ReplyAs1, {put_chars, unicode, Msg1}} -> | |||||
From1 ! {io_reply, ReplyAs1, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[debug]", TestMsg], re:split(Msg1, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
1000 -> | |||||
?assert(false) | |||||
end, | |||||
%% info is blacklisted | |||||
lager:error("Test message"), | |||||
receive | |||||
{io_request, From2, ReplyAs2, {put_chars, unicode, _Msg2}} -> | |||||
From2 ! {io_reply, ReplyAs2, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end | |||||
end | |||||
}, | |||||
{"console backend with custom group leader", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
gen_event:add_handler(lager_event, lager_console_backend, [{level, info}, {group_leader, Pid}]), | |||||
lager_config:set({lager_event, loglevel}, {element(2, lager_util:config_to_mask(info)), []}), | |||||
lager:info("Test message"), | |||||
?assertNotEqual({group_leader, Pid}, erlang:process_info(whereis(lager_event), group_leader)), | |||||
receive | |||||
{io_request, From1, ReplyAs1, {put_chars, unicode, Msg1}} -> | |||||
From1 ! {io_reply, ReplyAs1, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[info]", TestMsg], re:split(Msg1, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
1000 -> | |||||
?assert(false) | |||||
end, | |||||
%% killing the pid should prevent any new log messages (to prove we haven't accidentally redirected | |||||
%% the group leader some other way | |||||
exit(Pid, kill), | |||||
timer:sleep(100), | |||||
%% additionally, check the lager backend has been removed (because the group leader process died) | |||||
?assertNot(lists:member(lager_console_backend, gen_event:which_handlers(lager_event))), | |||||
lager:error("Test message"), | |||||
receive | |||||
{io_request, From2, ReplyAs2, {put_chars, unicode, _Msg2}} -> | |||||
From2 ! {io_reply, ReplyAs2, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end | |||||
end | |||||
}, | |||||
{"console backend with custom group leader using a trace and an ID", | |||||
fun() -> | |||||
Pid = spawn(F(self())), | |||||
ID = {?MODULE, trace_test}, | |||||
Handlers = lager_config:global_get(handlers, []), | |||||
HandlerInfo = lager_app:start_handler(lager_event, ID, | |||||
[{level, none}, {group_leader, Pid}, | |||||
{id, ID}]), | |||||
lager_config:global_set(handlers, [HandlerInfo|Handlers]), | |||||
lager:info("Test message"), | |||||
?assertNotEqual({group_leader, Pid}, erlang:process_info(whereis(lager_event), group_leader)), | |||||
receive | |||||
{io_request, From, ReplyAs, {put_chars, unicode, _Msg}} -> | |||||
From ! {io_reply, ReplyAs, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end, | |||||
lager:trace(ID, [{module, ?MODULE}], debug), | |||||
lager:info("Test message"), | |||||
receive | |||||
{io_request, From1, ReplyAs1, {put_chars, unicode, Msg1}} -> | |||||
From1 ! {io_reply, ReplyAs1, ok}, | |||||
TestMsg = "Test message" ++ eol(), | |||||
?assertMatch([_, "[info]", TestMsg], re:split(Msg1, " ", [{return, list}, {parts, 3}])) | |||||
after | |||||
500 -> | |||||
?assert(false) | |||||
end, | |||||
?assertNotEqual({0, []}, lager_config:get({lager_event, loglevel})), | |||||
%% killing the pid should prevent any new log messages (to prove we haven't accidentally redirected | |||||
%% the group leader some other way | |||||
exit(Pid, kill), | |||||
timer:sleep(100), | |||||
%% additionally, check the lager backend has been removed (because the group leader process died) | |||||
?assertNot(lists:member(lager_console_backend, gen_event:which_handlers(lager_event))), | |||||
%% finally, check the trace has been removed | |||||
?assertEqual({0, []}, lager_config:get({lager_event, loglevel})), | |||||
lager:error("Test message"), | |||||
receive | |||||
{io_request, From3, ReplyAs3, {put_chars, unicode, _Msg3}} -> | |||||
From3 ! {io_reply, ReplyAs3, ok}, | |||||
?assert(false) | |||||
after | |||||
500 -> | |||||
?assert(true) | |||||
end | |||||
end | |||||
} | |||||
] | |||||
}. | |||||
set_loglevel_test_() -> | |||||
{foreach, | |||||
fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_console_backend, [{level, info}]}]), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
lager:start() | |||||
end, | |||||
fun(_) -> | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end, | |||||
[ | |||||
{"Get/set loglevel test", | |||||
fun() -> | |||||
?assertEqual(info, lager:get_loglevel(lager_console_backend)), | |||||
lager:set_loglevel(lager_console_backend, debug), | |||||
?assertEqual(debug, lager:get_loglevel(lager_console_backend)), | |||||
lager:set_loglevel(lager_console_backend, '!=debug'), | |||||
?assertEqual(info, lager:get_loglevel(lager_console_backend)), | |||||
lager:set_loglevel(lager_console_backend, '!=info'), | |||||
?assertEqual(debug, lager:get_loglevel(lager_console_backend)), | |||||
ok | |||||
end | |||||
}, | |||||
{"Get/set invalid loglevel test", | |||||
fun() -> | |||||
?assertEqual(info, lager:get_loglevel(lager_console_backend)), | |||||
?assertEqual({error, bad_log_level}, | |||||
lager:set_loglevel(lager_console_backend, fatfinger)), | |||||
?assertEqual(info, lager:get_loglevel(lager_console_backend)) | |||||
end | |||||
} | |||||
] | |||||
}. | |||||
-endif. |
@ -0,0 +1,382 @@ | |||||
%% ------------------------------------------------------------------- | |||||
%% | |||||
%% 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 ~ts 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 ~ts: ~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 ~ts 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:create_test_dir(), | |||||
CrashLog = filename:join(TestDir, "crash_test.log"), | |||||
ok = lager_util:safe_write_file(CrashLog, []), | |||||
ok = error_logger:tty(false), | |||||
ok = lager_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() -> | |||||
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, lager_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} | |||||
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, lager_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} | |||||
end | |||||
]}. | |||||
-endif. | |||||
@ -0,0 +1,573 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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. | |||||
-module(lager_default_formatter). | |||||
%% | |||||
%% Include files | |||||
%% | |||||
-include("lager.hrl"). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
%% | |||||
%% Exported Functions | |||||
%% | |||||
-export([format/2, format/3]). | |||||
%% | |||||
%% API Functions | |||||
%% | |||||
%% @doc Provides a generic, default formatting for log messages using a semi-iolist as configuration. Any iolist allowed | |||||
%% elements in the configuration are printed verbatim. Atoms in the configuration are treated as metadata properties | |||||
%% and extracted from the log message. Optionally, a tuple of {atom(),semi-iolist()} can be used. The atom will look | |||||
%% up the property, but if not found it will use the semi-iolist() instead. These fallbacks can be similarly nested | |||||
%% or refer to other properties, if desired. You can also use a {atom, semi-iolist(), semi-iolist()} formatter, which | |||||
%% acts like a ternary operator's true/false branches. | |||||
%% | |||||
%% The metadata properties date,time, message, severity, and sev will always exist. | |||||
%% The properties pid, file, line, module, and function will always exist if the parser transform is used. | |||||
%% | |||||
%% Example: | |||||
%% | |||||
%% `["Foo"]' -> "Foo", regardless of message content. | |||||
%% | |||||
%% `[message]' -> The content of the logged message, alone. | |||||
%% | |||||
%% `[{pid,"Unknown Pid"}]' -> "?.?.?" if pid is in the metadata, "Unknown Pid" if not. | |||||
%% | |||||
%% `[{pid, ["My pid is ", pid], ["Unknown Pid"]}]' -> if pid is in the metada print "My pid is ?.?.?", otherwise print "Unknown Pid" | |||||
%% @end | |||||
-spec format(lager_msg:lager_msg(),list(),list()) -> any(). | |||||
format(Msg,[], Colors) -> | |||||
format(Msg, [{eol, "\n"}], Colors); | |||||
format(Msg,[{eol, EOL}], Colors) -> | |||||
Config = case application:get_env(lager, metadata_whitelist) of | |||||
undefined -> config(EOL, []); | |||||
{ok, Whitelist} -> config(EOL, Whitelist) | |||||
end, | |||||
format(Msg, Config, Colors); | |||||
format(Message,Config,Colors) -> | |||||
[ case V of | |||||
color -> output_color(Message,Colors); | |||||
_ -> output(V,Message) | |||||
end || V <- Config ]. | |||||
-spec format(lager_msg:lager_msg(),list()) -> any(). | |||||
format(Msg, Config) -> | |||||
format(Msg, Config, []). | |||||
-spec output(term(),lager_msg:lager_msg()) -> iolist(). | |||||
output(message,Msg) -> lager_msg:message(Msg); | |||||
output(date,Msg) -> | |||||
{D, _T} = lager_msg:datetime(Msg), | |||||
D; | |||||
output(time,Msg) -> | |||||
{_D, T} = lager_msg:datetime(Msg), | |||||
T; | |||||
output(severity,Msg) -> | |||||
atom_to_list(lager_msg:severity(Msg)); | |||||
output(severity_upper, Msg) -> | |||||
uppercase_severity(lager_msg:severity(Msg)); | |||||
output(blank,_Msg) -> | |||||
output({blank," "},_Msg); | |||||
output(node, _Msg) -> | |||||
output({node, atom_to_list(node())},_Msg); | |||||
output({blank,Fill},_Msg) -> | |||||
Fill; | |||||
output(sev,Msg) -> | |||||
%% Write brief acronym for the severity level (e.g. debug -> $D) | |||||
[lager_util:level_to_chr(lager_msg:severity(Msg))]; | |||||
output(metadata, Msg) -> | |||||
output({metadata, "=", " "}, Msg); | |||||
output({metadata, IntSep, FieldSep}, Msg) -> | |||||
MD = lists:keysort(1, lager_msg:metadata(Msg)), | |||||
string:join([io_lib:format("~s~s~p", [K, IntSep, V]) || {K, V} <- MD], FieldSep); | |||||
output({pterm, Key}, Msg) -> | |||||
output({pterm, Key, ""}, Msg); | |||||
output({pterm, Key, Default}, _Msg) -> | |||||
make_printable(maybe_get_persistent_term(Key, Default)); | |||||
output(Prop,Msg) when is_atom(Prop) -> | |||||
Metadata = lager_msg:metadata(Msg), | |||||
make_printable(get_metadata(Prop,Metadata,<<"Undefined">>)); | |||||
output({Prop,Default},Msg) when is_atom(Prop) -> | |||||
Metadata = lager_msg:metadata(Msg), | |||||
make_printable(get_metadata(Prop,Metadata,output(Default,Msg))); | |||||
output({Prop, Present, Absent}, Msg) when is_atom(Prop) -> | |||||
%% sort of like a poor man's ternary operator | |||||
Metadata = lager_msg:metadata(Msg), | |||||
case get_metadata(Prop, Metadata) of | |||||
undefined -> | |||||
[ output(V, Msg) || V <- Absent]; | |||||
_ -> | |||||
[ output(V, Msg) || V <- Present] | |||||
end; | |||||
output({Prop, Present, Absent, Width}, Msg) when is_atom(Prop) -> | |||||
%% sort of like a poor man's ternary operator | |||||
Metadata = lager_msg:metadata(Msg), | |||||
case get_metadata(Prop, Metadata) of | |||||
undefined -> | |||||
[ output(V, Msg, Width) || V <- Absent]; | |||||
_ -> | |||||
[ output(V, Msg, Width) || V <- Present] | |||||
end; | |||||
output(Other,_) -> make_printable(Other). | |||||
output(message, Msg, _Width) -> lager_msg:message(Msg); | |||||
output(date,Msg, _Width) -> | |||||
{D, _T} = lager_msg:datetime(Msg), | |||||
D; | |||||
output(time, Msg, _Width) -> | |||||
{_D, T} = lager_msg:datetime(Msg), | |||||
T; | |||||
output(severity, Msg, Width) -> | |||||
make_printable(atom_to_list(lager_msg:severity(Msg)), Width); | |||||
output(sev,Msg, _Width) -> | |||||
%% Write brief acronym for the severity level (e.g. debug -> $D) | |||||
[lager_util:level_to_chr(lager_msg:severity(Msg))]; | |||||
output(node, Msg, _Width) -> | |||||
output({node, atom_to_list(node())}, Msg, _Width); | |||||
output(blank,_Msg, _Width) -> | |||||
output({blank, " "},_Msg, _Width); | |||||
output({blank, Fill},_Msg, _Width) -> | |||||
Fill; | |||||
output(metadata, Msg, _Width) -> | |||||
output({metadata, "=", " "}, Msg, _Width); | |||||
output({metadata, IntSep, FieldSep}, Msg, _Width) -> | |||||
MD = lists:keysort(1, lager_msg:metadata(Msg)), | |||||
[string:join([io_lib:format("~s~s~p", [K, IntSep, V]) || {K, V} <- MD], FieldSep)]; | |||||
output({pterm, Key}, Msg, Width) -> | |||||
output({pterm, Key, ""}, Msg, Width); | |||||
output({pterm, Key, Default}, _Msg, _Width) -> | |||||
make_printable(maybe_get_persistent_term(Key, Default)); | |||||
output(Prop, Msg, Width) when is_atom(Prop) -> | |||||
Metadata = lager_msg:metadata(Msg), | |||||
make_printable(get_metadata(Prop,Metadata,<<"Undefined">>), Width); | |||||
output({Prop,Default},Msg, Width) when is_atom(Prop) -> | |||||
Metadata = lager_msg:metadata(Msg), | |||||
make_printable(get_metadata(Prop,Metadata,output(Default,Msg)), Width); | |||||
output(Other,_, Width) -> make_printable(Other, Width). | |||||
output_color(_Msg,[]) -> []; | |||||
output_color(Msg,Colors) -> | |||||
Level = lager_msg:severity(Msg), | |||||
case lists:keyfind(Level, 1, Colors) of | |||||
{_, Color} -> Color; | |||||
_ -> [] | |||||
end. | |||||
-spec make_printable(any()) -> iolist(). | |||||
make_printable(A) when is_atom(A) -> atom_to_list(A); | |||||
make_printable(P) when is_pid(P) -> pid_to_list(P); | |||||
make_printable(L) when is_list(L) orelse is_binary(L) -> L; | |||||
make_printable(Other) -> io_lib:format("~p",[Other]). | |||||
make_printable(A,W) when is_integer(W)-> string:left(make_printable(A),W); | |||||
make_printable(A,{Align,W}) when is_integer(W) -> | |||||
case Align of | |||||
left -> | |||||
string:left(make_printable(A),W); | |||||
centre -> | |||||
string:centre(make_printable(A),W); | |||||
right -> | |||||
string:right(make_printable(A),W); | |||||
_ -> | |||||
string:left(make_printable(A),W) | |||||
end; | |||||
make_printable(A,_W) -> make_printable(A). | |||||
%% persistent term was introduced in OTP 21.2, so | |||||
%% if we're running on an older OTP, just return the | |||||
%% default value. | |||||
-ifdef(OTP_RELEASE). | |||||
maybe_get_persistent_term(Key, Default) -> | |||||
try | |||||
persistent_term:get(Key, Default) | |||||
catch | |||||
_:undef -> Default | |||||
end. | |||||
-else. | |||||
maybe_get_persistent_term(_Key, Default) -> Default. | |||||
-endif. | |||||
run_function(Function, Default) -> | |||||
eRum = 1, | |||||
ERum = 2, | |||||
try Function() of | |||||
Result -> | |||||
Result | |||||
catch | |||||
_:_ -> | |||||
Default | |||||
end. | |||||
get_metadata(Key, Metadata) -> | |||||
get_metadata(Key, Metadata, undefined). | |||||
get_metadata(Key, Metadata, Default) -> | |||||
case lists:keyfind(Key, 1, Metadata) of | |||||
false -> | |||||
Default; | |||||
{Key, Value} when is_function(Value) -> | |||||
run_function(Value, Default); | |||||
{Key, Value} -> | |||||
Value | |||||
end. | |||||
config(EOL, []) -> | |||||
[ | |||||
date, " ", time, " ", color, "[", severity, "] ", | |||||
{pid, ""}, | |||||
{module, [ | |||||
{pid, ["@"], ""}, | |||||
module, | |||||
{function, [":", function], ""}, | |||||
{line, [":",line], ""}], ""}, | |||||
" ", message, EOL | |||||
]; | |||||
config(EOL, MetaWhitelist) -> | |||||
[ | |||||
date, " ", time, " ", color, "[", severity, "] ", | |||||
{pid, ""}, | |||||
{module, [ | |||||
{pid, ["@"], ""}, | |||||
module, | |||||
{function, [":", function], ""}, | |||||
{line, [":",line], ""}], ""}, | |||||
" " | |||||
] ++ | |||||
[{M, [atom_to_list(M), "=", M, " "], ""}|| M <- MetaWhitelist] ++ | |||||
[message, EOL]. | |||||
uppercase_severity(debug) -> "DEBUG"; | |||||
uppercase_severity(info) -> "INFO"; | |||||
uppercase_severity(notice) -> "NOTICE"; | |||||
uppercase_severity(warning) -> "WARNING"; | |||||
uppercase_severity(error) -> "ERROR"; | |||||
uppercase_severity(critical) -> "CRITICAL"; | |||||
uppercase_severity(alert) -> "ALERT"; | |||||
uppercase_severity(emergency) -> "EMERGENCY". | |||||
-ifdef(TEST). | |||||
date_time_now() -> | |||||
Now = os:timestamp(), | |||||
{Date, Time} = lager_util:format_time(lager_util:maybe_utc(lager_util:localtime_ms(Now))), | |||||
{Date, Time, Now}. | |||||
basic_test_() -> | |||||
{Date, Time, Now} = date_time_now(), | |||||
[{"Default formatting test", | |||||
?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] ", pid_to_list(self()), " Message\n"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, self()}], | |||||
[]), | |||||
[]))) | |||||
}, | |||||
{"Basic Formatting", | |||||
?_assertEqual(<<"Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, self()}], | |||||
[]), | |||||
["Simplist Format"]))) | |||||
}, | |||||
{"Default equivalent formatting test", | |||||
?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] ", pid_to_list(self()), " Message\n"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, self()}], | |||||
[]), | |||||
[date, " ", time," [",severity,"] ",pid, " ", message, "\n"] | |||||
))) | |||||
}, | |||||
{"Non existent metadata can default to string", | |||||
?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] Fallback Message\n"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, self()}], | |||||
[]), | |||||
[date, " ", time," [",severity,"] ",{does_not_exist,"Fallback"}, " ", message, "\n"] | |||||
))) | |||||
}, | |||||
{"Non existent metadata can default to other metadata", | |||||
?_assertEqual(iolist_to_binary([Date, " ", Time, " [error] Fallback Message\n"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, "Fallback"}], | |||||
[]), | |||||
[date, " ", time," [",severity,"] ",{does_not_exist,pid}, " ", message, "\n"] | |||||
))) | |||||
}, | |||||
{"Non existent metadata can default to a string2", | |||||
?_assertEqual(iolist_to_binary(["Unknown Pid"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[], | |||||
[]), | |||||
[{pid, ["My pid is ", pid], ["Unknown Pid"]}] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting", | |||||
?_assertEqual(iolist_to_binary(["My pid is hello"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
[{pid, ["My pid is ", pid], ["Unknown Pid"]}] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting1", | |||||
?_assertEqual(iolist_to_binary(["servername"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}, {server, servername}], | |||||
[]), | |||||
[{server,{pid, ["(", pid, ")"], ["(Unknown Server)"]}}] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting2", | |||||
?_assertEqual(iolist_to_binary(["(hello)"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
[{server,{pid, ["(", pid, ")"], ["(Unknown Server)"]}}] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting3", | |||||
?_assertEqual(iolist_to_binary(["(Unknown Server)"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[], | |||||
[]), | |||||
[{server,{pid, ["(", pid, ")"], ["(Unknown Server)"]}}] | |||||
))) | |||||
}, | |||||
{"Metadata can be printed in its enterity", | |||||
?_assertEqual(iolist_to_binary(["bar=2 baz=3 foo=1"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{foo, 1}, {bar, 2}, {baz, 3}], | |||||
[]), | |||||
[metadata] | |||||
))) | |||||
}, | |||||
{"Metadata can be printed in its enterity with custom seperators", | |||||
?_assertEqual(iolist_to_binary(["bar->2, baz->3, foo->1"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{foo, 1}, {bar, 2}, {baz, 3}], | |||||
[]), | |||||
[{metadata, "->", ", "}] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting with width 1", | |||||
?_assertEqual(iolist_to_binary(["(hello )(hello )(hello)(hello)(hello)"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
["(",{pid, [pid], "", 10},")", | |||||
"(",{pid, [pid], "", {bad_align,10}},")", | |||||
"(",{pid, [pid], "", bad10},")", | |||||
"(",{pid, [pid], "", {right,bad20}},")", | |||||
"(",{pid, [pid], "", {bad_align,bad20}},")"] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting with width 2", | |||||
?_assertEqual(iolist_to_binary(["(hello )"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
["(",{pid, [pid], "", {left,10}},")"] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting with width 3", | |||||
?_assertEqual(iolist_to_binary(["( hello)"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
["(",{pid, [pid], "", {right,10}},")"] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting with width 4", | |||||
?_assertEqual(iolist_to_binary(["( hello )"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
["(",{pid, [pid], "", {centre,10}},")"] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting with width 5", | |||||
?_assertEqual(iolist_to_binary(["error |hello ! ( hello )"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello}], | |||||
[]), | |||||
[{x,"",[severity,{blank,"|"},pid], 10},"!",blank,"(",{pid, [pid], "", {centre,10}},")"] | |||||
))) | |||||
}, | |||||
{"Metadata can have extra formatting with width 6", | |||||
?_assertEqual(iolist_to_binary([Time,Date," bar=2 baz=3 foo=1 pid=hello EMessage"]), | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, hello},{foo, 1}, {bar, 2}, {baz, 3}], | |||||
[]), | |||||
[{x,"",[time]}, {x,"",[date],20},blank,{x,"",[metadata],30},blank,{x,"",[sev],10},message, {message,message,"", {right,20}}] | |||||
))) | |||||
}, | |||||
{"Uppercase Severity Formatting - DEBUG", | |||||
?_assertEqual(<<"DEBUG Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
debug, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - INFO", | |||||
?_assertEqual(<<"INFO Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
info, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - NOTICE", | |||||
?_assertEqual(<<"NOTICE Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
notice, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - WARNING", | |||||
?_assertEqual(<<"WARNING Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
warning, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - ERROR", | |||||
?_assertEqual(<<"ERROR Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
error, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - CRITICAL", | |||||
?_assertEqual(<<"CRITICAL Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
critical, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - ALERT", | |||||
?_assertEqual(<<"ALERT Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
alert, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"Uppercase Severity Formatting - EMERGENCY", | |||||
?_assertEqual(<<"EMERGENCY Simplist Format">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
emergency, | |||||
[{pid, self()}], | |||||
[]), | |||||
[severity_upper, " Simplist Format"]))) | |||||
}, | |||||
{"pterm presence test", | |||||
%% skip test on OTP < 21 | |||||
case list_to_integer(erlang:system_info(otp_release)) >= 21 of | |||||
true -> | |||||
?_assertEqual(<<"Pterm is: something">>, | |||||
begin | |||||
persistent_term:put(thing, something), | |||||
Ret = iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
emergency, | |||||
[{pid, self()}], | |||||
[]), | |||||
["Pterm is: ", {pterm, thing}])), | |||||
persistent_term:erase(thing), | |||||
Ret | |||||
end); | |||||
false -> ?_assert(true) | |||||
end | |||||
}, | |||||
{"pterm absence test", | |||||
?_assertEqual(<<"Pterm is: nothing">>, | |||||
iolist_to_binary(format(lager_msg:new("Message", | |||||
Now, | |||||
emergency, | |||||
[{pid, self()}], | |||||
[]), | |||||
["Pterm is: ", {pterm, thing, "nothing"}]))) | |||||
}, | |||||
{"node formatting basic", | |||||
begin | |||||
[N, "foo"] = format(lager_msg:new("Message", | |||||
Now, | |||||
info, | |||||
[{pid, self()}], | |||||
[]), | |||||
[node, "foo"]), | |||||
?_assertNotMatch(nomatch, re:run(N, <<"@">>)) | |||||
end | |||||
} | |||||
]. | |||||
-endif. |
@ -0,0 +1,544 @@ | |||||
%% | |||||
%% %CopyrightBegin% | |||||
%% | |||||
%% Copyright Ericsson AB 1996-2011-2012. All Rights Reserved. | |||||
%% | |||||
%% The contents of this file are subject to the Erlang Public License, | |||||
%% Version 1.1, (the "License"); you may not use this file except in | |||||
%% compliance with the License. You should have received a copy of the | |||||
%% Erlang Public License along with this software. If not, it can be | |||||
%% retrieved online at http://www.erlang.org/. | |||||
%% | |||||
%% Software distributed under the License is distributed on an "AS IS" | |||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See | |||||
%% the License for the specific language governing rights and limitations | |||||
%% under the License. | |||||
%% | |||||
%% %CopyrightEnd% | |||||
%% | |||||
-module(lager_format). | |||||
%% fork of io_lib_format that uses trunc_io to protect against large terms | |||||
-export([format/3, format/4]). | |||||
-record(options, { | |||||
chomp = false :: boolean() | |||||
}). | |||||
format(FmtStr, Args, MaxLen) -> | |||||
format(FmtStr, Args, MaxLen, []). | |||||
format([], [], _, _) -> | |||||
""; | |||||
format(FmtStr, Args, MaxLen, Opts) when is_atom(FmtStr) -> | |||||
format(atom_to_list(FmtStr), Args, MaxLen, Opts); | |||||
format(FmtStr, Args, MaxLen, Opts) when is_binary(FmtStr) -> | |||||
format(binary_to_list(FmtStr), Args, MaxLen, Opts); | |||||
format(FmtStr, Args, MaxLen, Opts) when is_list(FmtStr) -> | |||||
case io_lib:deep_char_list(FmtStr) of | |||||
true -> | |||||
Options = make_options(Opts, #options{}), | |||||
Cs = collect(FmtStr, Args), | |||||
{Cs2, MaxLen2} = build(Cs, [], MaxLen, Options), | |||||
%% count how many terms remain | |||||
{Count, StrLen} = lists:foldl( | |||||
fun({_C, _As, _F, _Adj, _P, _Pad, _Enc}, {Terms, Chars}) -> | |||||
{Terms + 1, Chars}; | |||||
(_, {Terms, Chars}) -> | |||||
{Terms, Chars + 1} | |||||
end, {0, 0}, Cs2), | |||||
build2(Cs2, Count, MaxLen2 - StrLen); | |||||
false -> | |||||
erlang:error(badarg) | |||||
end; | |||||
format(_FmtStr, _Args, _MaxLen, _Opts) -> | |||||
erlang:error(badarg). | |||||
collect([$~|Fmt0], Args0) -> | |||||
{C,Fmt1,Args1} = collect_cseq(Fmt0, Args0), | |||||
[C|collect(Fmt1, Args1)]; | |||||
collect([C|Fmt], Args) -> | |||||
[C|collect(Fmt, Args)]; | |||||
collect([], []) -> []. | |||||
collect_cseq(Fmt0, Args0) -> | |||||
{F,Ad,Fmt1,Args1} = field_width(Fmt0, Args0), | |||||
{P,Fmt2,Args2} = precision(Fmt1, Args1), | |||||
{Pad,Fmt3,Args3} = pad_char(Fmt2, Args2), | |||||
{Encoding,Fmt4,Args4} = encoding(Fmt3, Args3), | |||||
{C,As,Fmt5,Args5} = collect_cc(Fmt4, Args4), | |||||
{{C,As,F,Ad,P,Pad,Encoding},Fmt5,Args5}. | |||||
encoding([$t|Fmt],Args) -> | |||||
{unicode,Fmt,Args}; | |||||
encoding(Fmt,Args) -> | |||||
{latin1,Fmt,Args}. | |||||
field_width([$-|Fmt0], Args0) -> | |||||
{F,Fmt,Args} = field_value(Fmt0, Args0), | |||||
field_width(-F, Fmt, Args); | |||||
field_width(Fmt0, Args0) -> | |||||
{F,Fmt,Args} = field_value(Fmt0, Args0), | |||||
field_width(F, Fmt, Args). | |||||
field_width(F, Fmt, Args) when F < 0 -> | |||||
{-F,left,Fmt,Args}; | |||||
field_width(F, Fmt, Args) when F >= 0 -> | |||||
{F,right,Fmt,Args}. | |||||
precision([$.|Fmt], Args) -> | |||||
field_value(Fmt, Args); | |||||
precision(Fmt, Args) -> | |||||
{none,Fmt,Args}. | |||||
field_value([$*|Fmt], [A|Args]) when is_integer(A) -> | |||||
{A,Fmt,Args}; | |||||
field_value([C|Fmt], Args) when is_integer(C), C >= $0, C =< $9 -> | |||||
field_value([C|Fmt], Args, 0); | |||||
field_value(Fmt, Args) -> | |||||
{none,Fmt,Args}. | |||||
field_value([C|Fmt], Args, F) when is_integer(C), C >= $0, C =< $9 -> | |||||
field_value(Fmt, Args, 10*F + (C - $0)); | |||||
field_value(Fmt, Args, F) -> %Default case | |||||
{F,Fmt,Args}. | |||||
pad_char([$.,$*|Fmt], [Pad|Args]) -> {Pad,Fmt,Args}; | |||||
pad_char([$.,Pad|Fmt], Args) -> {Pad,Fmt,Args}; | |||||
pad_char(Fmt, Args) -> {$\s,Fmt,Args}. | |||||
%% collect_cc([FormatChar], [Argument]) -> | |||||
%% {Control,[ControlArg],[FormatChar],[Arg]}. | |||||
%% Here we collect the argments for each control character. | |||||
%% Be explicit to cause failure early. | |||||
collect_cc([$w|Fmt], [A|Args]) -> {$w,[A],Fmt,Args}; | |||||
collect_cc([$p|Fmt], [A|Args]) -> {$p,[A],Fmt,Args}; | |||||
collect_cc([$W|Fmt], [A,Depth|Args]) -> {$W,[A,Depth],Fmt,Args}; | |||||
collect_cc([$P|Fmt], [A,Depth|Args]) -> {$P,[A,Depth],Fmt,Args}; | |||||
collect_cc([$s|Fmt], [A|Args]) -> {$s,[A],Fmt,Args}; | |||||
collect_cc([$e|Fmt], [A|Args]) -> {$e,[A],Fmt,Args}; | |||||
collect_cc([$f|Fmt], [A|Args]) -> {$f,[A],Fmt,Args}; | |||||
collect_cc([$g|Fmt], [A|Args]) -> {$g,[A],Fmt,Args}; | |||||
collect_cc([$b|Fmt], [A|Args]) -> {$b,[A],Fmt,Args}; | |||||
collect_cc([$B|Fmt], [A|Args]) -> {$B,[A],Fmt,Args}; | |||||
collect_cc([$x|Fmt], [A,Prefix|Args]) -> {$x,[A,Prefix],Fmt,Args}; | |||||
collect_cc([$X|Fmt], [A,Prefix|Args]) -> {$X,[A,Prefix],Fmt,Args}; | |||||
collect_cc([$+|Fmt], [A|Args]) -> {$+,[A],Fmt,Args}; | |||||
collect_cc([$#|Fmt], [A|Args]) -> {$#,[A],Fmt,Args}; | |||||
collect_cc([$c|Fmt], [A|Args]) -> {$c,[A],Fmt,Args}; | |||||
collect_cc([$~|Fmt], Args) when is_list(Args) -> {$~,[],Fmt,Args}; | |||||
collect_cc([$n|Fmt], Args) when is_list(Args) -> {$n,[],Fmt,Args}; | |||||
collect_cc([$i|Fmt], [A|Args]) -> {$i,[A],Fmt,Args}. | |||||
%% build([Control], Pc, Indentation) -> [Char]. | |||||
%% Interpret the control structures. Count the number of print | |||||
%% remaining and only calculate indentation when necessary. Must also | |||||
%% be smart when calculating indentation for characters in format. | |||||
build([{$n, _, _, _, _, _, _}], Acc, MaxLen, #options{chomp=true}) -> | |||||
%% trailing ~n, ignore | |||||
{lists:reverse(Acc), MaxLen}; | |||||
build([{C,As,F,Ad,P,Pad,Enc}|Cs], Acc, MaxLen, O) -> | |||||
{S, MaxLen2} = control(C, As, F, Ad, P, Pad, Enc, MaxLen), | |||||
build(Cs, [S|Acc], MaxLen2, O); | |||||
build([$\n], Acc, MaxLen, #options{chomp=true}) -> | |||||
%% trailing \n, ignore | |||||
{lists:reverse(Acc), MaxLen}; | |||||
build([$\n|Cs], Acc, MaxLen, O) -> | |||||
build(Cs, [$\n|Acc], MaxLen - 1, O); | |||||
build([$\t|Cs], Acc, MaxLen, O) -> | |||||
build(Cs, [$\t|Acc], MaxLen - 1, O); | |||||
build([C|Cs], Acc, MaxLen, O) -> | |||||
build(Cs, [C|Acc], MaxLen - 1, O); | |||||
build([], Acc, MaxLen, _O) -> | |||||
{lists:reverse(Acc), MaxLen}. | |||||
build2([{C,As,F,Ad,P,Pad,Enc}|Cs], Count, MaxLen) -> | |||||
{S, Len} = control2(C, As, F, Ad, P, Pad, Enc, MaxLen div Count), | |||||
[S|build2(Cs, Count - 1, MaxLen - Len)]; | |||||
build2([C|Cs], Count, MaxLen) -> | |||||
[C|build2(Cs, Count, MaxLen)]; | |||||
build2([], _, _) -> []. | |||||
%% control(FormatChar, [Argument], FieldWidth, Adjust, Precision, PadChar, | |||||
%% Indentation) -> [Char] | |||||
%% This is the main dispatch function for the various formatting commands. | |||||
%% Field widths and precisions have already been calculated. | |||||
control($e, [A], F, Adj, P, Pad, _Enc, L) when is_float(A) -> | |||||
Res = fwrite_e(A, F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($f, [A], F, Adj, P, Pad, _Enc, L) when is_float(A) -> | |||||
Res = fwrite_f(A, F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($g, [A], F, Adj, P, Pad, _Enc, L) when is_float(A) -> | |||||
Res = fwrite_g(A, F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($b, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
Res = unprefixed_integer(A, F, Adj, base(P), Pad, true), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($B, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
Res = unprefixed_integer(A, F, Adj, base(P), Pad, false), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($x, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A), | |||||
is_atom(Prefix) -> | |||||
Res = prefixed_integer(A, F, Adj, base(P), Pad, atom_to_list(Prefix), true), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($x, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
true = io_lib:deep_char_list(Prefix), %Check if Prefix a character list | |||||
Res = prefixed_integer(A, F, Adj, base(P), Pad, Prefix, true), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($X, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A), | |||||
is_atom(Prefix) -> | |||||
Res = prefixed_integer(A, F, Adj, base(P), Pad, atom_to_list(Prefix), false), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($X, [A,Prefix], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
true = io_lib:deep_char_list(Prefix), %Check if Prefix a character list | |||||
Res = prefixed_integer(A, F, Adj, base(P), Pad, Prefix, false), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($+, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
Base = base(P), | |||||
Prefix = [integer_to_list(Base), $#], | |||||
Res = prefixed_integer(A, F, Adj, Base, Pad, Prefix, true), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($#, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
Base = base(P), | |||||
Prefix = [integer_to_list(Base), $#], | |||||
Res = prefixed_integer(A, F, Adj, Base, Pad, Prefix, false), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($c, [A], F, Adj, P, Pad, unicode, L) when is_integer(A) -> | |||||
Res = char(A, F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($c, [A], F, Adj, P, Pad, _Enc, L) when is_integer(A) -> | |||||
Res = char(A band 255, F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($~, [], F, Adj, P, Pad, _Enc, L) -> | |||||
Res = char($~, F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($n, [], F, Adj, P, Pad, _Enc, L) -> | |||||
Res = newline(F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control($i, [_A], _F, _Adj, _P, _Pad, _Enc, L) -> | |||||
{[], L}; | |||||
control($s, [A], F, Adj, P, Pad, _Enc, L) when is_atom(A) -> | |||||
Res = string(atom_to_list(A), F, Adj, P, Pad), | |||||
{Res, L - lists:flatlength(Res)}; | |||||
control(C, A, F, Adj, P, Pad, Enc, L) -> | |||||
%% save this for later - these are all the 'large' terms | |||||
{{C, A, F, Adj, P, Pad, Enc}, L}. | |||||
control2($w, [A], F, Adj, P, Pad, _Enc, L) -> | |||||
Term = lager_trunc_io:fprint(A, L, [{lists_as_strings, false}]), | |||||
Res = term(Term, F, Adj, P, Pad), | |||||
{Res, lists:flatlength(Res)}; | |||||
control2($p, [A], _F, _Adj, _P, _Pad, _Enc, L) -> | |||||
Term = lager_trunc_io:fprint(A, L, [{lists_as_strings, true}]), | |||||
{Term, lists:flatlength(Term)}; | |||||
control2($W, [A,Depth], F, Adj, P, Pad, _Enc, L) when is_integer(Depth) -> | |||||
Term = lager_trunc_io:fprint(A, L, [{depth, Depth}, {lists_as_strings, false}]), | |||||
Res = term(Term, F, Adj, P, Pad), | |||||
{Res, lists:flatlength(Res)}; | |||||
control2($P, [A,Depth], _F, _Adj, _P, _Pad, _Enc, L) when is_integer(Depth) -> | |||||
Term = lager_trunc_io:fprint(A, L, [{depth, Depth}, {lists_as_strings, true}]), | |||||
{Term, lists:flatlength(Term)}; | |||||
control2($s, [L0], F, Adj, P, Pad, latin1, L) -> | |||||
List = lager_trunc_io:fprint(iolist_to_chars(L0), L, [{force_strings, true}]), | |||||
Res = string(List, F, Adj, P, Pad), | |||||
{Res, lists:flatlength(Res)}; | |||||
control2($s, [L0], F, Adj, P, Pad, unicode, L) -> | |||||
List = lager_trunc_io:fprint(cdata_to_chars(L0), L, [{force_strings, true}]), | |||||
Res = uniconv(string(List, F, Adj, P, Pad)), | |||||
{Res, lists:flatlength(Res)}. | |||||
iolist_to_chars([C|Cs]) when is_integer(C), C >= $\000, C =< $\377 -> | |||||
[C | iolist_to_chars(Cs)]; | |||||
iolist_to_chars([I|Cs]) -> | |||||
[iolist_to_chars(I) | iolist_to_chars(Cs)]; | |||||
iolist_to_chars([]) -> | |||||
[]; | |||||
iolist_to_chars(B) when is_binary(B) -> | |||||
binary_to_list(B). | |||||
cdata_to_chars([C|Cs]) when is_integer(C), C >= $\000 -> | |||||
[C | cdata_to_chars(Cs)]; | |||||
cdata_to_chars([I|Cs]) -> | |||||
[cdata_to_chars(I) | cdata_to_chars(Cs)]; | |||||
cdata_to_chars([]) -> | |||||
[]; | |||||
cdata_to_chars(B) when is_binary(B) -> | |||||
case catch unicode:characters_to_list(B) of | |||||
L when is_list(L) -> L; | |||||
_ -> binary_to_list(B) | |||||
end. | |||||
make_options([], Options) -> | |||||
Options; | |||||
make_options([{chomp, Bool}|T], Options) when is_boolean(Bool) -> | |||||
make_options(T, Options#options{chomp=Bool}). | |||||
-ifdef(UNICODE_AS_BINARIES). | |||||
uniconv(C) -> | |||||
unicode:characters_to_binary(C,unicode). | |||||
-else. | |||||
uniconv(C) -> | |||||
C. | |||||
-endif. | |||||
%% Default integer base | |||||
base(none) -> | |||||
10; | |||||
base(B) when is_integer(B) -> | |||||
B. | |||||
%% term(TermList, Field, Adjust, Precision, PadChar) | |||||
%% Output the characters in a term. | |||||
%% Adjust the characters within the field if length less than Max padding | |||||
%% with PadChar. | |||||
term(T, none, _Adj, none, _Pad) -> T; | |||||
term(T, none, Adj, P, Pad) -> term(T, P, Adj, P, Pad); | |||||
term(T, F, Adj, P0, Pad) -> | |||||
L = lists:flatlength(T), | |||||
P = case P0 of none -> erlang:min(L, F); _ -> P0 end, | |||||
if | |||||
L > P -> | |||||
adjust(chars($*, P), chars(Pad, F-P), Adj); | |||||
F >= P -> | |||||
adjust(T, chars(Pad, F-L), Adj) | |||||
end. | |||||
%% fwrite_e(Float, Field, Adjust, Precision, PadChar) | |||||
fwrite_e(Fl, none, Adj, none, Pad) -> %Default values | |||||
fwrite_e(Fl, none, Adj, 6, Pad); | |||||
fwrite_e(Fl, none, _Adj, P, _Pad) when P >= 2 -> | |||||
float_e(Fl, float_data(Fl), P); | |||||
fwrite_e(Fl, F, Adj, none, Pad) -> | |||||
fwrite_e(Fl, F, Adj, 6, Pad); | |||||
fwrite_e(Fl, F, Adj, P, Pad) when P >= 2 -> | |||||
term(float_e(Fl, float_data(Fl), P), F, Adj, F, Pad). | |||||
float_e(Fl, Fd, P) when Fl < 0.0 -> %Negative numbers | |||||
[$-|float_e(-Fl, Fd, P)]; | |||||
float_e(_Fl, {Ds,E}, P) -> | |||||
case float_man(Ds, 1, P-1) of | |||||
{[$0|Fs],true} -> [[$1|Fs]|float_exp(E)]; | |||||
{Fs,false} -> [Fs|float_exp(E-1)] | |||||
end. | |||||
%% float_man([Digit], Icount, Dcount) -> {[Chars],CarryFlag}. | |||||
%% Generate the characters in the mantissa from the digits with Icount | |||||
%% characters before the '.' and Dcount decimals. Handle carry and let | |||||
%% caller decide what to do at top. | |||||
float_man(Ds, 0, Dc) -> | |||||
{Cs,C} = float_man(Ds, Dc), | |||||
{[$.|Cs],C}; | |||||
float_man([D|Ds], I, Dc) -> | |||||
case float_man(Ds, I-1, Dc) of | |||||
{Cs,true} when D =:= $9 -> {[$0|Cs],true}; | |||||
{Cs,true} -> {[D+1|Cs],false}; | |||||
{Cs,false} -> {[D|Cs],false} | |||||
end; | |||||
float_man([], I, Dc) -> %Pad with 0's | |||||
{string:chars($0, I, [$.|string:chars($0, Dc)]),false}. | |||||
float_man([D|_], 0) when D >= $5 -> {[],true}; | |||||
float_man([_|_], 0) -> {[],false}; | |||||
float_man([D|Ds], Dc) -> | |||||
case float_man(Ds, Dc-1) of | |||||
{Cs,true} when D =:= $9 -> {[$0|Cs],true}; | |||||
{Cs,true} -> {[D+1|Cs],false}; | |||||
{Cs,false} -> {[D|Cs],false} | |||||
end; | |||||
float_man([], Dc) -> {string:chars($0, Dc),false}. %Pad with 0's | |||||
%% float_exp(Exponent) -> [Char]. | |||||
%% Generate the exponent of a floating point number. Always include sign. | |||||
float_exp(E) when E >= 0 -> | |||||
[$e,$+|integer_to_list(E)]; | |||||
float_exp(E) -> | |||||
[$e|integer_to_list(E)]. | |||||
%% fwrite_f(FloatData, Field, Adjust, Precision, PadChar) | |||||
fwrite_f(Fl, none, Adj, none, Pad) -> %Default values | |||||
fwrite_f(Fl, none, Adj, 6, Pad); | |||||
fwrite_f(Fl, none, _Adj, P, _Pad) when P >= 1 -> | |||||
float_f(Fl, float_data(Fl), P); | |||||
fwrite_f(Fl, F, Adj, none, Pad) -> | |||||
fwrite_f(Fl, F, Adj, 6, Pad); | |||||
fwrite_f(Fl, F, Adj, P, Pad) when P >= 1 -> | |||||
term(float_f(Fl, float_data(Fl), P), F, Adj, F, Pad). | |||||
float_f(Fl, Fd, P) when Fl < 0.0 -> | |||||
[$-|float_f(-Fl, Fd, P)]; | |||||
float_f(Fl, {Ds,E}, P) when E =< 0 -> | |||||
float_f(Fl, {string:chars($0, -E+1, Ds),1}, P); %Prepend enough 0's | |||||
float_f(_Fl, {Ds,E}, P) -> | |||||
case float_man(Ds, E, P) of | |||||
{Fs,true} -> "1" ++ Fs; %Handle carry | |||||
{Fs,false} -> Fs | |||||
end. | |||||
%% float_data([FloatChar]) -> {[Digit],Exponent} | |||||
float_data(Fl) -> | |||||
float_data(float_to_list(Fl), []). | |||||
float_data([$e|E], Ds) -> | |||||
{lists:reverse(Ds),list_to_integer(E)+1}; | |||||
float_data([D|Cs], Ds) when D >= $0, D =< $9 -> | |||||
float_data(Cs, [D|Ds]); | |||||
float_data([_|Cs], Ds) -> | |||||
float_data(Cs, Ds). | |||||
%% fwrite_g(Float, Field, Adjust, Precision, PadChar) | |||||
%% Use the f form if Float is >= 0.1 and < 1.0e4, | |||||
%% and the prints correctly in the f form, else the e form. | |||||
%% Precision always means the # of significant digits. | |||||
fwrite_g(Fl, F, Adj, none, Pad) -> | |||||
fwrite_g(Fl, F, Adj, 6, Pad); | |||||
fwrite_g(Fl, F, Adj, P, Pad) when P >= 1 -> | |||||
A = abs(Fl), | |||||
E = if A < 1.0e-1 -> -2; | |||||
A < 1.0e0 -> -1; | |||||
A < 1.0e1 -> 0; | |||||
A < 1.0e2 -> 1; | |||||
A < 1.0e3 -> 2; | |||||
A < 1.0e4 -> 3; | |||||
true -> fwrite_f | |||||
end, | |||||
if P =< 1, E =:= -1; | |||||
P-1 > E, E >= -1 -> | |||||
fwrite_f(Fl, F, Adj, P-1-E, Pad); | |||||
P =< 1 -> | |||||
fwrite_e(Fl, F, Adj, 2, Pad); | |||||
true -> | |||||
fwrite_e(Fl, F, Adj, P, Pad) | |||||
end. | |||||
%% string(String, Field, Adjust, Precision, PadChar) | |||||
string(S, none, _Adj, none, _Pad) -> S; | |||||
string(S, F, Adj, none, Pad) -> | |||||
string_field(S, F, Adj, lists:flatlength(S), Pad); | |||||
string(S, none, _Adj, P, Pad) -> | |||||
string_field(S, P, left, lists:flatlength(S), Pad); | |||||
string(S, F, Adj, P, Pad) when F >= P -> | |||||
N = lists:flatlength(S), | |||||
if F > P -> | |||||
if N > P -> | |||||
adjust(flat_trunc(S, P), chars(Pad, F-P), Adj); | |||||
N < P -> | |||||
adjust([S|chars(Pad, P-N)], chars(Pad, F-P), Adj); | |||||
true -> % N == P | |||||
adjust(S, chars(Pad, F-P), Adj) | |||||
end; | |||||
true -> % F == P | |||||
string_field(S, F, Adj, N, Pad) | |||||
end. | |||||
string_field(S, F, _Adj, N, _Pad) when N > F -> | |||||
flat_trunc(S, F); | |||||
string_field(S, F, Adj, N, Pad) when N < F -> | |||||
adjust(S, chars(Pad, F-N), Adj); | |||||
string_field(S, _, _, _, _) -> % N == F | |||||
S. | |||||
%% unprefixed_integer(Int, Field, Adjust, Base, PadChar, Lowercase) | |||||
%% -> [Char]. | |||||
unprefixed_integer(Int, F, Adj, Base, Pad, Lowercase) | |||||
when Base >= 2, Base =< 1+$Z-$A+10 -> | |||||
if Int < 0 -> | |||||
S = cond_lowercase(erlang:integer_to_list(-Int, Base), Lowercase), | |||||
term([$-|S], F, Adj, none, Pad); | |||||
true -> | |||||
S = cond_lowercase(erlang:integer_to_list(Int, Base), Lowercase), | |||||
term(S, F, Adj, none, Pad) | |||||
end. | |||||
%% prefixed_integer(Int, Field, Adjust, Base, PadChar, Prefix, Lowercase) | |||||
%% -> [Char]. | |||||
prefixed_integer(Int, F, Adj, Base, Pad, Prefix, Lowercase) | |||||
when Base >= 2, Base =< 1+$Z-$A+10 -> | |||||
if Int < 0 -> | |||||
S = cond_lowercase(erlang:integer_to_list(-Int, Base), Lowercase), | |||||
term([$-,Prefix|S], F, Adj, none, Pad); | |||||
true -> | |||||
S = cond_lowercase(erlang:integer_to_list(Int, Base), Lowercase), | |||||
term([Prefix|S], F, Adj, none, Pad) | |||||
end. | |||||
%% char(Char, Field, Adjust, Precision, PadChar) -> [Char]. | |||||
char(C, none, _Adj, none, _Pad) -> [C]; | |||||
char(C, F, _Adj, none, _Pad) -> chars(C, F); | |||||
char(C, none, _Adj, P, _Pad) -> chars(C, P); | |||||
char(C, F, Adj, P, Pad) when F >= P -> | |||||
adjust(chars(C, P), chars(Pad, F - P), Adj). | |||||
%% newline(Field, Adjust, Precision, PadChar) -> [Char]. | |||||
newline(none, _Adj, _P, _Pad) -> "\n"; | |||||
newline(F, right, _P, _Pad) -> chars($\n, F). | |||||
%% | |||||
%% Utilities | |||||
%% | |||||
adjust(Data, [], _) -> Data; | |||||
adjust(Data, Pad, left) -> [Data|Pad]; | |||||
adjust(Data, Pad, right) -> [Pad|Data]. | |||||
%% Flatten and truncate a deep list to at most N elements. | |||||
flat_trunc(List, N) when is_integer(N), N >= 0 -> | |||||
flat_trunc(List, N, []). | |||||
flat_trunc(L, 0, R) when is_list(L) -> | |||||
lists:reverse(R); | |||||
flat_trunc([H|T], N, R) -> | |||||
flat_trunc(T, N-1, [H|R]); | |||||
flat_trunc([], _, R) -> | |||||
lists:reverse(R). | |||||
%% A deep version of string:chars/2,3 | |||||
chars(_C, 0) -> | |||||
[]; | |||||
chars(C, 1) -> | |||||
[C]; | |||||
chars(C, 2) -> | |||||
[C,C]; | |||||
chars(C, 3) -> | |||||
[C,C,C]; | |||||
chars(C, N) when is_integer(N), (N band 1) =:= 0 -> | |||||
S = chars(C, N bsr 1), | |||||
[S|S]; | |||||
chars(C, N) when is_integer(N) -> | |||||
S = chars(C, N bsr 1), | |||||
[C,S|S]. | |||||
%chars(C, N, Tail) -> | |||||
% [chars(C, N)|Tail]. | |||||
%% Lowercase conversion | |||||
cond_lowercase(String, true) -> | |||||
lowercase(String); | |||||
cond_lowercase(String,false) -> | |||||
String. | |||||
lowercase([H|T]) when is_integer(H), H >= $A, H =< $Z -> | |||||
[(H-$A+$a)|lowercase(T)]; | |||||
lowercase([H|T]) -> | |||||
[H|lowercase(T)]; | |||||
lowercase([]) -> | |||||
[]. |
@ -0,0 +1,243 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 A process that does a gen_event:add_sup_handler and attempts to re-add | |||||
%% event handlers when they exit. | |||||
%% @private | |||||
-module(lager_handler_watcher). | |||||
-behaviour(gen_server). | |||||
-include("lager.hrl"). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-export([pop_until/2]). | |||||
-endif. | |||||
%% callbacks | |||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, | |||||
code_change/3]). | |||||
-export([start_link/3, start/3]). | |||||
-record(state, { | |||||
module :: atom(), | |||||
config :: any(), | |||||
sink :: pid() | atom() | |||||
}). | |||||
start_link(Sink, Module, Config) -> | |||||
gen_server:start_link(?MODULE, [Sink, Module, Config], []). | |||||
start(Sink, Module, Config) -> | |||||
gen_server:start(?MODULE, [Sink, Module, Config], []). | |||||
init([Sink, Module, Config]) -> | |||||
process_flag(trap_exit, true), | |||||
install_handler(Sink, Module, Config), | |||||
{ok, #state{sink=Sink, module=Module, config=Config}}. | |||||
handle_call(_Call, _From, State) -> | |||||
{reply, ok, State}. | |||||
handle_cast(_Request, State) -> | |||||
{noreply, State}. | |||||
handle_info({gen_event_EXIT, Module, normal}, #state{module=Module} = State) -> | |||||
{stop, normal, State}; | |||||
handle_info({gen_event_EXIT, Module, shutdown}, #state{module=Module} = State) -> | |||||
{stop, normal, State}; | |||||
handle_info({gen_event_EXIT, Module, {'EXIT', {kill_me, [_KillerHWM, KillerReinstallAfter]}}}, | |||||
#state{module=Module, sink=Sink, config = Config} = State) -> | |||||
%% Brutally kill the manager but stay alive to restore settings. | |||||
%% | |||||
%% SinkPid here means the gen_event process. Handlers *all* live inside the | |||||
%% same gen_event process space, so when the Pid is killed, *all* of the | |||||
%% pending log messages in its mailbox will die too. | |||||
SinkPid = whereis(Sink), | |||||
unlink(SinkPid), | |||||
{message_queue_len, Len} = process_info(SinkPid, message_queue_len), | |||||
error_logger:error_msg("Killing sink ~p, current message_queue_len:~p~n", [Sink, Len]), | |||||
exit(SinkPid, kill), | |||||
_ = timer:apply_after(KillerReinstallAfter, lager_app, start_handler, [Sink, Module, Config]), | |||||
{stop, normal, State}; | |||||
handle_info({gen_event_EXIT, Module, Reason}, #state{module=Module, | |||||
config=Config, sink=Sink} = State) -> | |||||
case lager:log(error, self(), "Lager event handler ~p exited with reason ~s", | |||||
[Module, error_logger_lager_h:format_reason(Reason)]) of | |||||
ok -> | |||||
install_handler(Sink, Module, Config); | |||||
{error, _} -> | |||||
%% lager is not working, so installing a handler won't work | |||||
ok | |||||
end, | |||||
{noreply, State}; | |||||
handle_info(reinstall_handler, #state{module=Module, config=Config, sink=Sink} = State) -> | |||||
install_handler(Sink, Module, Config), | |||||
{noreply, State}; | |||||
handle_info({reboot, Sink}, State) -> | |||||
_ = lager_app:boot(Sink), | |||||
{noreply, State}; | |||||
handle_info(stop, State) -> | |||||
{stop, normal, State}; | |||||
handle_info({'EXIT', _Pid, killed}, #state{module=Module, config=Config, sink=Sink} = State) -> | |||||
Tmr = application:get_env(lager, killer_reinstall_after, 5000), | |||||
_ = timer:apply_after(Tmr, lager_app, start_handler, [Sink, Module, Config]), | |||||
{stop, normal, State}; | |||||
handle_info(_Info, State) -> | |||||
{noreply, State}. | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. | |||||
%% internal | |||||
install_handler(Sink, lager_backend_throttle, Config) -> | |||||
%% The lager_backend_throttle needs to know to which sink it is | |||||
%% attached, hence this admittedly ugly workaround. Handlers are | |||||
%% sensitive to the structure of the configuration sent to `init', | |||||
%% sadly, so it's not trivial to add a configuration item to be | |||||
%% ignored to backends without breaking 3rd party handlers. | |||||
install_handler2(Sink, lager_backend_throttle, [{sink, Sink}|Config]); | |||||
install_handler(Sink, Module, Config) -> | |||||
install_handler2(Sink, Module, Config). | |||||
%% private | |||||
install_handler2(Sink, Module, Config) -> | |||||
case gen_event:add_sup_handler(Sink, Module, Config) of | |||||
ok -> | |||||
?INT_LOG(debug, "Lager installed handler ~p into ~p", [Module, Sink]), | |||||
lager:update_loglevel_config(Sink), | |||||
ok; | |||||
{error, {fatal, Reason}} -> | |||||
?INT_LOG(error, "Lager fatally failed to install handler ~p into" | |||||
" ~p, NOT retrying: ~p", [Module, Sink, Reason]), | |||||
%% tell ourselves to stop | |||||
self() ! stop, | |||||
ok; | |||||
Error -> | |||||
%% try to reinstall it later | |||||
?INT_LOG(error, "Lager failed to install handler ~p into" | |||||
" ~p, retrying later : ~p", [Module, Sink, Error]), | |||||
erlang:send_after(5000, self(), reinstall_handler), | |||||
ok | |||||
end. | |||||
-ifdef(TEST). | |||||
from_now(Seconds) -> | |||||
{Mega, Secs, Micro} = os:timestamp(), | |||||
{Mega, Secs + Seconds, Micro}. | |||||
reinstall_on_initial_failure_test_() -> | |||||
{timeout, 60000, | |||||
[ | |||||
fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_test_backend, info}, {lager_crash_backend, [from_now(2), undefined]}]), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
application:unset_env(lager, crash_log), | |||||
lager:start(), | |||||
try | |||||
{_Level, _Time, Message, _Metadata} = lager_test_backend:pop(), | |||||
?assertMatch("Lager failed to install handler lager_crash_backend into lager_event, retrying later :"++_, lists:flatten(Message)), | |||||
timer:sleep(6000), | |||||
lager_test_backend:flush(), | |||||
?assertEqual(0, lager_test_backend:count()), | |||||
?assert(lists:member(lager_crash_backend, gen_event:which_handlers(lager_event))) | |||||
after | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end | |||||
end | |||||
] | |||||
}. | |||||
reinstall_on_runtime_failure_test_() -> | |||||
{timeout, 60000, | |||||
[ | |||||
fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_test_backend, info}, {lager_crash_backend, [undefined, from_now(5)]}]), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
application:unset_env(lager, crash_log), | |||||
lager:start(), | |||||
try | |||||
?assert(lists:member(lager_crash_backend, gen_event:which_handlers(lager_event))), | |||||
timer:sleep(6000), | |||||
pop_until("Lager event handler lager_crash_backend exited with reason crash", fun lists:flatten/1), | |||||
pop_until("Lager failed to install handler lager_crash_backend into lager_event, retrying later", | |||||
fun(Msg) -> string:substr(lists:flatten(Msg), 1, 84) end), | |||||
?assertEqual(false, lists:member(lager_crash_backend, gen_event:which_handlers(lager_event))) | |||||
after | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end | |||||
end | |||||
] | |||||
}. | |||||
reinstall_handlers_after_killer_hwm_test_() -> | |||||
{timeout, 60000, | |||||
[ | |||||
fun() -> | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [{lager_manager_killer, [1000, 5000]}]), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
application:set_env(lager, killer_reinstall_after, 5000), | |||||
application:unset_env(lager, crash_log), | |||||
lager:start(), | |||||
lager:trace_file("foo", [{foo, "bar"}], error), | |||||
L = length(gen_event:which_handlers(lager_event)), | |||||
try | |||||
lager_manager_killer:kill_me(), | |||||
timer:sleep(6000), | |||||
?assertEqual(L, length(gen_event:which_handlers(lager_event))), | |||||
file:delete("foo") | |||||
after | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(true) | |||||
end | |||||
end | |||||
] | |||||
}. | |||||
pop_until(String, Fun) -> | |||||
try_backend_pop(lager_test_backend:pop(), String, Fun). | |||||
try_backend_pop(undefined, String, _Fun) -> | |||||
throw("Not found: " ++ String); | |||||
try_backend_pop({_Severity, _Date, Msg, _Metadata}, String, Fun) -> | |||||
case Fun(Msg) of | |||||
String -> | |||||
ok; | |||||
_ -> | |||||
try_backend_pop(lager_test_backend:pop(), String, Fun) | |||||
end. | |||||
-endif. |
@ -0,0 +1,39 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 A supervisor for monitoring lager_handler_watcher processes. | |||||
%% @private | |||||
-module(lager_handler_watcher_sup). | |||||
-behaviour(supervisor). | |||||
%% API | |||||
-export([start_link/0]). | |||||
%% Callbacks | |||||
-export([init/1]). | |||||
start_link() -> | |||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []). | |||||
init([]) -> | |||||
{ok, {{simple_one_for_one, 10, 60}, | |||||
[ | |||||
{lager_handler_watcher, {lager_handler_watcher, start_link, []}, | |||||
temporary, 5000, worker, [lager_handler_watcher]} | |||||
]}}. |
@ -0,0 +1,53 @@ | |||||
-module(lager_manager_killer). | |||||
-author("Sungjin Park <jinni.park@gmail.com>"). | |||||
-behavior(gen_event). | |||||
-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2, code_change/3]). | |||||
-export([kill_me/0]). | |||||
-include("lager.hrl"). | |||||
-record(state, { | |||||
killer_hwm :: non_neg_integer(), | |||||
killer_reinstall_after :: non_neg_integer() | |||||
}). | |||||
kill_me() -> | |||||
gen_event:call(lager_event, ?MODULE, kill_self). | |||||
init([KillerHWM, KillerReinstallAfter]) -> | |||||
{ok, #state{killer_hwm=KillerHWM, killer_reinstall_after=KillerReinstallAfter}}. | |||||
handle_call(get_loglevel, State) -> | |||||
{ok, {mask, ?LOG_NONE}, State}; | |||||
handle_call({set_loglevel, _Level}, State) -> | |||||
{ok, ok, State}; | |||||
handle_call(get_settings, State = #state{killer_hwm=KillerHWM, killer_reinstall_after=KillerReinstallAfter}) -> | |||||
{ok, [KillerHWM, KillerReinstallAfter], State}; | |||||
handle_call(kill_self, #state{killer_hwm=KillerHWM, killer_reinstall_after=KillerReinstallAfter}) -> | |||||
exit({kill_me, [KillerHWM, KillerReinstallAfter]}); | |||||
handle_call(_Request, State) -> | |||||
{ok, ok, State}. | |||||
%% It's not the best idea in the world to check the queue length for every | |||||
%% log message. We can make this operation work on a poll timer in the | |||||
%% future. | |||||
handle_event({log, _Message}, State = #state{killer_hwm=KillerHWM, killer_reinstall_after=KillerReinstallAfter}) -> | |||||
{message_queue_len, Len} = process_info(self(), message_queue_len), | |||||
case Len > KillerHWM of | |||||
true -> | |||||
exit({kill_me, [KillerHWM, KillerReinstallAfter]}); | |||||
_ -> | |||||
{ok, State} | |||||
end; | |||||
handle_event(_Event, State) -> | |||||
{ok, State}. | |||||
handle_info(_Info, State) -> | |||||
{ok, State}. | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. |
@ -0,0 +1,64 @@ | |||||
-module(lager_msg). | |||||
-export([new/4, new/5]). | |||||
-export([message/1]). | |||||
-export([timestamp/1]). | |||||
-export([datetime/1]). | |||||
-export([severity/1]). | |||||
-export([severity_as_int/1]). | |||||
-export([metadata/1]). | |||||
-export([destinations/1]). | |||||
-record(lager_msg,{ | |||||
destinations :: list(), | |||||
metadata :: [tuple()], | |||||
severity :: lager:log_level(), | |||||
datetime :: {string(), string()}, | |||||
timestamp :: erlang:timestamp(), | |||||
message :: list() | |||||
}). | |||||
-opaque lager_msg() :: #lager_msg{}. | |||||
-export_type([lager_msg/0]). | |||||
%% create with provided timestamp, handy for testing mostly | |||||
-spec new(list(), erlang:timestamp(), lager:log_level(), [tuple()], list()) -> lager_msg(). | |||||
new(Msg, Timestamp, Severity, Metadata, Destinations) -> | |||||
{Date, Time} = lager_util:format_time(lager_util:maybe_utc(lager_util:localtime_ms(Timestamp))), | |||||
#lager_msg{message=Msg, datetime={Date, Time}, timestamp=Timestamp, severity=Severity, | |||||
metadata=Metadata, destinations=Destinations}. | |||||
-spec new(list(), lager:log_level(), [tuple()], list()) -> lager_msg(). | |||||
new(Msg, Severity, Metadata, Destinations) -> | |||||
Now = os:timestamp(), | |||||
new(Msg, Now, Severity, Metadata, Destinations). | |||||
-spec message(lager_msg()) -> list(). | |||||
message(Msg) -> | |||||
Msg#lager_msg.message. | |||||
-spec timestamp(lager_msg()) -> erlang:timestamp(). | |||||
timestamp(Msg) -> | |||||
Msg#lager_msg.timestamp. | |||||
-spec datetime(lager_msg()) -> {string(), string()}. | |||||
datetime(Msg) -> | |||||
Msg#lager_msg.datetime. | |||||
-spec severity(lager_msg()) -> lager:log_level(). | |||||
severity(Msg) -> | |||||
Msg#lager_msg.severity. | |||||
-spec severity_as_int(lager_msg()) -> lager:log_level_number(). | |||||
severity_as_int(Msg) -> | |||||
lager_util:level_to_num(Msg#lager_msg.severity). | |||||
-spec metadata(lager_msg()) -> [tuple()]. | |||||
metadata(Msg) -> | |||||
Msg#lager_msg.metadata. | |||||
-spec destinations(lager_msg()) -> list(). | |||||
destinations(Msg) -> | |||||
Msg#lager_msg.destinations. | |||||
@ -0,0 +1,18 @@ | |||||
-module(lager_rotator_behaviour). | |||||
%% Create a log file | |||||
-callback(create_logfile(Name::list(), Buffer::{integer(), integer()} | any()) -> | |||||
{ok, {file:io_device(), integer(), file:date_time(), integer()}} | {error, any()}). | |||||
%% Open a log file | |||||
-callback(open_logfile(Name::list(), Buffer::{integer(), integer()} | any()) -> | |||||
{ok, {file:io_device(), integer(), file:date_time(), integer()}} | {error, any()}). | |||||
%% Ensure reference to current target, could be rotated | |||||
-callback(ensure_logfile(Name::list(), FD::file:io_device(), Inode::integer(), Ctime::file:date_time(), | |||||
Buffer::{integer(), integer()} | any()) -> | |||||
{ok, {file:io_device(), integer(), file:date_time(), integer()}} | {error, any()}). | |||||
%% Rotate the log file | |||||
-callback(rotate_logfile(Name::list(), Count::integer()) -> | |||||
ok). |
@ -0,0 +1,197 @@ | |||||
-module(lager_rotator_default). | |||||
-include_lib("kernel/include/file.hrl"). | |||||
-behaviour(lager_rotator_behaviour). | |||||
-export([ | |||||
create_logfile/2, open_logfile/2, ensure_logfile/5, rotate_logfile/2 | |||||
]). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
create_logfile(Name, Buffer) -> | |||||
open_logfile(Name, Buffer). | |||||
open_logfile(Name, Buffer) -> | |||||
case filelib:ensure_dir(Name) of | |||||
ok -> | |||||
Options = [append, raw] ++ | |||||
case Buffer of | |||||
{Size0, Interval} when is_integer(Interval), Interval >= 0, is_integer(Size0), Size0 >= 0 -> | |||||
[{delayed_write, Size0, Interval}]; | |||||
_ -> [] | |||||
end, | |||||
case file:open(Name, Options) of | |||||
{ok, FD} -> | |||||
case file:read_file_info(Name, [raw]) of | |||||
{ok, FInfo0} -> | |||||
Inode = FInfo0#file_info.inode, | |||||
{ok, Ctime} = maybe_update_ctime(Name, FInfo0), | |||||
Size1 = FInfo0#file_info.size, | |||||
{ok, {FD, Inode, Ctime, Size1}}; | |||||
X -> X | |||||
end; | |||||
Y -> Y | |||||
end; | |||||
Z -> Z | |||||
end. | |||||
ensure_logfile(Name, undefined, _Inode, _Ctime, Buffer) -> | |||||
open_logfile(Name, Buffer); | |||||
ensure_logfile(Name, FD, Inode0, Ctime0, Buffer) -> | |||||
case lager_util:has_file_changed(Name, Inode0, Ctime0) of | |||||
{true, _FInfo} -> | |||||
reopen_logfile(Name, FD, Buffer); | |||||
{_, FInfo} -> | |||||
{ok, {FD, Inode0, Ctime0, FInfo#file_info.size}} | |||||
end. | |||||
reopen_logfile(Name, FD0, Buffer) -> | |||||
%% Flush and close any file handles. | |||||
%% delayed write can cause file:close not to do a close | |||||
_ = file:datasync(FD0), | |||||
_ = file:close(FD0), | |||||
_ = file:close(FD0), | |||||
case open_logfile(Name, Buffer) of | |||||
{ok, {_FD1, _Inode, _Size, _Ctime}=FileInfo} -> | |||||
%% inode changed, file was probably moved and | |||||
%% recreated | |||||
{ok, FileInfo}; | |||||
Error -> | |||||
Error | |||||
end. | |||||
%% renames failing are OK | |||||
rotate_logfile(File, 0) -> | |||||
%% open the file in write-only mode to truncate/create it | |||||
case file:open(File, [write]) of | |||||
{ok, FD} -> | |||||
_ = file:close(FD), | |||||
_ = file:close(FD), | |||||
{ok, _Ctime} = maybe_update_ctime(File), | |||||
ok; | |||||
Error -> | |||||
Error | |||||
end; | |||||
rotate_logfile(File0, 1) -> | |||||
File1 = File0 ++ ".0", | |||||
_ = file:rename(File0, File1), | |||||
rotate_logfile(File0, 0); | |||||
rotate_logfile(File0, Count) -> | |||||
File1 = File0 ++ "." ++ integer_to_list(Count - 2), | |||||
File2 = File0 ++ "." ++ integer_to_list(Count - 1), | |||||
_ = file:rename(File1, File2), | |||||
rotate_logfile(File0, Count - 1). | |||||
maybe_update_ctime(Name) -> | |||||
case file:read_file_info(Name, [raw]) of | |||||
{ok, FInfo} -> | |||||
maybe_update_ctime(Name, FInfo); | |||||
_ -> | |||||
{ok, calendar:local_time()} | |||||
end. | |||||
maybe_update_ctime(Name, FInfo) -> | |||||
{OsType, _} = os:type(), | |||||
do_update_ctime(OsType, Name, FInfo). | |||||
do_update_ctime(win32, Name, FInfo0) -> | |||||
% Note: we force the creation time to be the current time. | |||||
% On win32 this may prevent the ctime from being updated: | |||||
% https://stackoverflow.com/q/8804342/1466825 | |||||
NewCtime = calendar:local_time(), | |||||
FInfo1 = FInfo0#file_info{ctime = NewCtime}, | |||||
ok = file:write_file_info(Name, FInfo1, [raw]), | |||||
{ok, NewCtime}; | |||||
do_update_ctime(_, _Name, FInfo) -> | |||||
{ok, FInfo#file_info.ctime}. | |||||
-ifdef(TEST). | |||||
rotate_file_test() -> | |||||
RotCount = 10, | |||||
{ok, TestDir} = lager_util:create_test_dir(), | |||||
TestLog = filename:join(TestDir, "rotation.log"), | |||||
Outer = fun(N) -> | |||||
?assertEqual(ok, lager_util:safe_write_file(TestLog, erlang:integer_to_list(N))), | |||||
Inner = fun(M) -> | |||||
File = lists:flatten([TestLog, $., erlang:integer_to_list(M)]), | |||||
?assert(filelib:is_regular(File)), | |||||
%% check the expected value is in the file | |||||
Number = erlang:list_to_binary(integer_to_list(N - M - 1)), | |||||
?assertEqual({ok, Number}, file:read_file(File)) | |||||
end, | |||||
Count = erlang:min(N, RotCount), | |||||
% The first time through, Count == 0, so the sequence is empty, | |||||
% effectively skipping the inner loop so a rotation can occur that | |||||
% creates the file that Inner looks for. | |||||
% Don't shoot the messenger, it was worse before this refactoring. | |||||
lists:foreach(Inner, lists:seq(0, Count-1)), | |||||
rotate_logfile(TestLog, RotCount) | |||||
end, | |||||
lists:foreach(Outer, lists:seq(0, (RotCount * 2))), | |||||
lager_util:delete_test_dir(TestDir). | |||||
rotate_file_zero_count_test() -> | |||||
%% Test that a rotation count of 0 simply truncates the file | |||||
{ok, TestDir} = lager_util:create_test_dir(), | |||||
TestLog = filename:join(TestDir, "rotation.log"), | |||||
?assertMatch(ok, rotate_logfile(TestLog, 0)), | |||||
?assertNot(filelib:is_regular(TestLog ++ ".0")), | |||||
?assertEqual(true, filelib:is_regular(TestLog)), | |||||
?assertEqual(1, length(filelib:wildcard(TestLog++"*"))), | |||||
%% assert the new file is 0 size: | |||||
case file:read_file_info(TestLog, [raw]) of | |||||
{ok, FInfo} -> | |||||
?assertEqual(0, FInfo#file_info.size); | |||||
_ -> | |||||
?assert(false) | |||||
end, | |||||
lager_util:delete_test_dir(TestDir). | |||||
rotate_file_fail_test() -> | |||||
{ok, TestDir} = lager_util:create_test_dir(), | |||||
TestLog = filename:join(TestDir, "rotation.log"), | |||||
%% set known permissions on it | |||||
ok = lager_util:set_dir_permissions("u+rwx", TestDir), | |||||
%% write a file | |||||
?assertEqual(ok, lager_util:safe_write_file(TestLog, "hello")), | |||||
case os:type() of | |||||
{win32, _} -> ok; | |||||
_ -> | |||||
%% hose up the permissions | |||||
ok = lager_util:set_dir_permissions("u-w", TestDir), | |||||
?assertMatch({error, _}, rotate_logfile(TestLog, 10)) | |||||
end, | |||||
%% check we still only have one file, rotation.log | |||||
?assertEqual([TestLog], filelib:wildcard(TestLog++"*")), | |||||
?assert(filelib:is_regular(TestLog)), | |||||
%% fix the permissions | |||||
ok = lager_util:set_dir_permissions("u+w", TestDir), | |||||
?assertMatch(ok, rotate_logfile(TestLog, 10)), | |||||
?assert(filelib:is_regular(TestLog ++ ".0")), | |||||
?assertEqual(true, filelib:is_regular(TestLog)), | |||||
?assertEqual(2, length(filelib:wildcard(TestLog++"*"))), | |||||
%% assert the new file is 0 size: | |||||
case file:read_file_info(TestLog, [raw]) of | |||||
{ok, FInfo} -> | |||||
?assertEqual(0, FInfo#file_info.size); | |||||
_ -> | |||||
?assert(false) | |||||
end, | |||||
%% check that the .0 file now has the contents "hello" | |||||
?assertEqual({ok, <<"hello">>}, file:read_file(TestLog++".0")), | |||||
lager_util:delete_test_dir(TestDir). | |||||
-endif. |
@ -0,0 +1,502 @@ | |||||
%% | |||||
%% %CopyrightBegin% | |||||
%% | |||||
%% Copyright Ericsson AB 1996-2009. All Rights Reserved. | |||||
%% | |||||
%% The contents of this file are subject to the Erlang Public License, | |||||
%% Version 1.1, (the "License"); you may not use this file except in | |||||
%% compliance with the License. You should have received a copy of the | |||||
%% Erlang Public License along with this software. If not, it can be | |||||
%% retrieved online at http://www.erlang.org/. | |||||
%% | |||||
%% Software distributed under the License is distributed on an "AS IS" | |||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See | |||||
%% the License for the specific language governing rights and limitations | |||||
%% under the License. | |||||
%% | |||||
%% %CopyrightEnd% | |||||
%% | |||||
%% @doc Functions from Erlang OTP distribution that are really useful | |||||
%% but aren't exported. | |||||
%% | |||||
%% All functions in this module are covered by the Erlang/OTP source | |||||
%% distribution's license, the Erlang Public License. See | |||||
%% http://www.erlang.org/ for full details. | |||||
-module(lager_stdlib). | |||||
-export([string_p/1]). | |||||
-export([write_time/2, maybe_utc/1]). | |||||
-export([is_my_error_report/1, is_my_info_report/1]). | |||||
-export([sup_get/2]). | |||||
-export([proc_lib_format/2]). | |||||
%% from error_logger_file_h | |||||
string_p([]) -> | |||||
false; | |||||
string_p(Term) -> | |||||
string_p1(Term). | |||||
string_p1([H|T]) when is_integer(H), H >= $\s, H < 256 -> | |||||
string_p1(T); | |||||
string_p1([$\n|T]) -> string_p1(T); | |||||
string_p1([$\r|T]) -> string_p1(T); | |||||
string_p1([$\t|T]) -> string_p1(T); | |||||
string_p1([$\v|T]) -> string_p1(T); | |||||
string_p1([$\b|T]) -> string_p1(T); | |||||
string_p1([$\f|T]) -> string_p1(T); | |||||
string_p1([$\e|T]) -> string_p1(T); | |||||
string_p1([H|T]) when is_list(H) -> | |||||
case string_p1(H) of | |||||
true -> string_p1(T); | |||||
_ -> false | |||||
end; | |||||
string_p1([]) -> true; | |||||
string_p1(_) -> false. | |||||
%% From calendar | |||||
-type year1970() :: 1970..10000. % should probably be 1970.. | |||||
-type month() :: 1..12. | |||||
-type day() :: 1..31. | |||||
-type hour() :: 0..23. | |||||
-type minute() :: 0..59. | |||||
-type second() :: 0..59. | |||||
-type t_time() :: {hour(),minute(),second()}. | |||||
-type t_datetime1970() :: {{year1970(),month(),day()},t_time()}. | |||||
%% From OTP stdlib's error_logger_tty_h.erl ... These functions aren't | |||||
%% exported. | |||||
-spec write_time({utc, t_datetime1970()} | t_datetime1970(), string()) -> string(). | |||||
write_time({utc,{{Y,Mo,D},{H,Mi,S}}},Type) -> | |||||
io_lib:format("~n=~s==== ~p-~s-~p::~s:~s:~s UTC ===~n", | |||||
[Type,D,month(Mo),Y,t(H),t(Mi),t(S)]); | |||||
write_time({{Y,Mo,D},{H,Mi,S}},Type) -> | |||||
io_lib:format("~n=~s==== ~p-~s-~p::~s:~s:~s ===~n", | |||||
[Type,D,month(Mo),Y,t(H),t(Mi),t(S)]). | |||||
-spec maybe_utc(t_datetime1970()) -> {utc, t_datetime1970()} | t_datetime1970(). | |||||
maybe_utc(Time) -> | |||||
UTC = case application:get_env(sasl, utc_log) of | |||||
{ok, Val} -> | |||||
Val; | |||||
undefined -> | |||||
%% Backwards compatible: | |||||
application:get_env(stdlib, utc_log, false) | |||||
end, | |||||
if | |||||
UTC =:= true -> | |||||
UTCTime = case calendar:local_time_to_universal_time_dst(Time) of | |||||
[] -> calendar:local_time(); | |||||
[T0|_] -> T0 | |||||
end, | |||||
{utc, UTCTime}; | |||||
true -> | |||||
Time | |||||
end. | |||||
t(X) when is_integer(X) -> | |||||
t1(integer_to_list(X)); | |||||
t(_) -> | |||||
"". | |||||
t1([X]) -> [$0,X]; | |||||
t1(X) -> X. | |||||
month(1) -> "Jan"; | |||||
month(2) -> "Feb"; | |||||
month(3) -> "Mar"; | |||||
month(4) -> "Apr"; | |||||
month(5) -> "May"; | |||||
month(6) -> "Jun"; | |||||
month(7) -> "Jul"; | |||||
month(8) -> "Aug"; | |||||
month(9) -> "Sep"; | |||||
month(10) -> "Oct"; | |||||
month(11) -> "Nov"; | |||||
month(12) -> "Dec". | |||||
%% From OTP sasl's sasl_report.erl ... These functions aren't | |||||
%% exported. | |||||
-spec is_my_error_report(atom()) -> boolean(). | |||||
is_my_error_report(supervisor_report) -> true; | |||||
is_my_error_report(crash_report) -> true; | |||||
is_my_error_report(_) -> false. | |||||
-spec is_my_info_report(atom()) -> boolean(). | |||||
is_my_info_report(progress) -> true; | |||||
is_my_info_report(_) -> false. | |||||
-spec sup_get(term(), [proplists:property()]) -> term(). | |||||
sup_get(Tag, Report) -> | |||||
case lists:keysearch(Tag, 1, Report) of | |||||
{value, {_, Value}} -> | |||||
Value; | |||||
_ -> | |||||
"" | |||||
end. | |||||
%% From OTP stdlib's proc_lib.erl ... These functions aren't exported. | |||||
-spec proc_lib_format([term()], pos_integer()) -> string(). | |||||
proc_lib_format([OwnReport,LinkReport], FmtMaxBytes) -> | |||||
OwnFormat = format_report(OwnReport, FmtMaxBytes), | |||||
LinkFormat = format_report(LinkReport, FmtMaxBytes), | |||||
%% io_lib:format here is OK because we're limiting max length elsewhere. | |||||
Str = io_lib:format(" crasher:~n~s neighbours:~n~s",[OwnFormat,LinkFormat]), | |||||
lists:flatten(Str). | |||||
format_report(Rep, FmtMaxBytes) when is_list(Rep) -> | |||||
format_rep(Rep, FmtMaxBytes); | |||||
format_report(Rep, FmtMaxBytes) -> | |||||
{Str, _} = lager_trunc_io:print(Rep, FmtMaxBytes), | |||||
io_lib:format("~p~n", [Str]). | |||||
format_rep([{initial_call,InitialCall}|Rep], FmtMaxBytes) -> | |||||
[format_mfa(InitialCall, FmtMaxBytes)|format_rep(Rep, FmtMaxBytes)]; | |||||
format_rep([{error_info,{Class,Reason,StackTrace}}|Rep], FmtMaxBytes) -> | |||||
[format_exception(Class, Reason, StackTrace, FmtMaxBytes)|format_rep(Rep, FmtMaxBytes)]; | |||||
format_rep([{Tag,Data}|Rep], FmtMaxBytes) -> | |||||
[format_tag(Tag, Data, FmtMaxBytes)|format_rep(Rep, FmtMaxBytes)]; | |||||
format_rep(_, _S) -> | |||||
[]. | |||||
format_exception(Class, Reason, StackTrace, FmtMaxBytes) -> | |||||
PF = pp_fun(FmtMaxBytes), | |||||
StackFun = fun(M, _F, _A) -> (M =:= erl_eval) or (M =:= ?MODULE) end, | |||||
%% EI = " exception: ", | |||||
EI = " ", | |||||
[EI, lib_format_exception(1+length(EI), Class, Reason, | |||||
StackTrace, StackFun, PF), "\n"]. | |||||
format_mfa({M,F,Args}=StartF, FmtMaxBytes) -> | |||||
try | |||||
A = length(Args), | |||||
[" initial call: ",atom_to_list(M),$:,atom_to_list(F),$/, | |||||
integer_to_list(A),"\n"] | |||||
catch | |||||
error:_ -> | |||||
format_tag(initial_call, StartF, FmtMaxBytes) | |||||
end. | |||||
pp_fun(FmtMaxBytes) -> | |||||
fun(Term, _I) -> | |||||
{Str, _} = lager_trunc_io:print(Term, FmtMaxBytes), | |||||
io_lib:format("~s", [Str]) | |||||
end. | |||||
format_tag(Tag, Data, FmtMaxBytes) -> | |||||
{Str, _} = lager_trunc_io:print(Data, FmtMaxBytes), | |||||
io_lib:format(" ~p: ~s~n", [Tag, Str]). | |||||
%% From OTP stdlib's lib.erl ... These functions aren't exported. | |||||
lib_format_exception(I, Class, Reason, StackTrace, StackFun, FormatFun) | |||||
when is_integer(I), I >= 1, is_function(StackFun, 3), | |||||
is_function(FormatFun, 2) -> | |||||
Str = n_spaces(I-1), | |||||
{Term,Trace1,Trace} = analyze_exception(Class, Reason, StackTrace), | |||||
Expl0 = explain_reason(Term, Class, Trace1, FormatFun, Str), | |||||
Expl = io_lib:fwrite(<<"~s~s">>, [exited(Class), Expl0]), | |||||
case format_stacktrace1(Str, Trace, FormatFun, StackFun) of | |||||
[] -> Expl; | |||||
Stack -> [Expl, $\n, Stack] | |||||
end. | |||||
analyze_exception(error, Term, Stack) -> | |||||
case {is_stacktrace(Stack), Stack, Term} of | |||||
{true, [{_M,_F,As}=MFA|MFAs], function_clause} when is_list(As) -> | |||||
{Term,[MFA],MFAs}; | |||||
{true, [{shell,F,A}], function_clause} when is_integer(A) -> | |||||
{Term, [{F,A}], []}; | |||||
{true, [{_M,_F,_AorAs}=MFA|MFAs], undef} -> | |||||
{Term,[MFA],MFAs}; | |||||
{true, _, _} -> | |||||
{Term,[],Stack}; | |||||
{false, _, _} -> | |||||
{{Term,Stack},[],[]} | |||||
end; | |||||
analyze_exception(_Class, Term, Stack) -> | |||||
case is_stacktrace(Stack) of | |||||
true -> | |||||
{Term,[],Stack}; | |||||
false -> | |||||
{{Term,Stack},[],[]} | |||||
end. | |||||
is_stacktrace([]) -> | |||||
true; | |||||
is_stacktrace([{M,F,A}|Fs]) when is_atom(M), is_atom(F), is_integer(A) -> | |||||
is_stacktrace(Fs); | |||||
is_stacktrace([{M,F,As}|Fs]) when is_atom(M), is_atom(F), length(As) >= 0 -> | |||||
is_stacktrace(Fs); | |||||
is_stacktrace(_) -> | |||||
false. | |||||
%% ERTS exit codes (some of them are also returned by erl_eval): | |||||
explain_reason(badarg, error, [], _PF, _Str) -> | |||||
<<"bad argument">>; | |||||
explain_reason({badarg,V}, error=Cl, [], PF, Str) -> % orelse, andalso | |||||
format_value(V, <<"bad argument: ">>, Cl, PF, Str); | |||||
explain_reason(badarith, error, [], _PF, _Str) -> | |||||
<<"bad argument in an arithmetic expression">>; | |||||
explain_reason({badarity,{Fun,As}}, error, [], _PF, _Str) | |||||
when is_function(Fun) -> | |||||
%% Only the arity is displayed, not the arguments As. | |||||
io_lib:fwrite(<<"~s called with ~s">>, | |||||
[format_fun(Fun), argss(length(As))]); | |||||
explain_reason({badfun,Term}, error=Cl, [], PF, Str) -> | |||||
format_value(Term, <<"bad function ">>, Cl, PF, Str); | |||||
explain_reason({badmatch,Term}, error=Cl, [], PF, Str) -> | |||||
format_value(Term, <<"no match of right hand side value ">>, Cl, PF, Str); | |||||
explain_reason({case_clause,V}, error=Cl, [], PF, Str) -> | |||||
%% "there is no case clause with a true guard sequence and a | |||||
%% pattern matching..." | |||||
format_value(V, <<"no case clause matching ">>, Cl, PF, Str); | |||||
explain_reason(function_clause, error, [{F,A}], _PF, _Str) -> | |||||
%% Shell commands | |||||
FAs = io_lib:fwrite(<<"~w/~w">>, [F, A]), | |||||
[<<"no function clause matching call to ">> | FAs]; | |||||
explain_reason(function_clause, error=Cl, [{M,F,As}], PF, Str) -> | |||||
String = <<"no function clause matching ">>, | |||||
format_errstr_call(String, Cl, {M,F}, As, PF, Str); | |||||
explain_reason(if_clause, error, [], _PF, _Str) -> | |||||
<<"no true branch found when evaluating an if expression">>; | |||||
explain_reason(noproc, error, [], _PF, _Str) -> | |||||
<<"no such process or port">>; | |||||
explain_reason(notalive, error, [], _PF, _Str) -> | |||||
<<"the node cannot be part of a distributed system">>; | |||||
explain_reason(system_limit, error, [], _PF, _Str) -> | |||||
<<"a system limit has been reached">>; | |||||
explain_reason(timeout_value, error, [], _PF, _Str) -> | |||||
<<"bad receive timeout value">>; | |||||
explain_reason({try_clause,V}, error=Cl, [], PF, Str) -> | |||||
%% "there is no try clause with a true guard sequence and a | |||||
%% pattern matching..." | |||||
format_value(V, <<"no try clause matching ">>, Cl, PF, Str); | |||||
explain_reason(undef, error, [{M,F,A}], _PF, _Str) -> | |||||
%% Only the arity is displayed, not the arguments, if there are any. | |||||
io_lib:fwrite(<<"undefined function ~s">>, | |||||
[mfa_to_string(M, F, n_args(A))]); | |||||
explain_reason({shell_undef,F,A}, error, [], _PF, _Str) -> | |||||
%% Give nicer reports for undefined shell functions | |||||
%% (but not when the user actively calls shell_default:F(...)). | |||||
io_lib:fwrite(<<"undefined shell command ~s/~w">>, [F, n_args(A)]); | |||||
%% Exit codes returned by erl_eval only: | |||||
explain_reason({argument_limit,_Fun}, error, [], _PF, _Str) -> | |||||
io_lib:fwrite(<<"limit of number of arguments to interpreted function" | |||||
" exceeded">>, []); | |||||
explain_reason({bad_filter,V}, error=Cl, [], PF, Str) -> | |||||
format_value(V, <<"bad filter ">>, Cl, PF, Str); | |||||
explain_reason({bad_generator,V}, error=Cl, [], PF, Str) -> | |||||
format_value(V, <<"bad generator ">>, Cl, PF, Str); | |||||
explain_reason({unbound,V}, error, [], _PF, _Str) -> | |||||
io_lib:fwrite(<<"variable ~w is unbound">>, [V]); | |||||
%% Exit codes local to the shell module (restricted shell): | |||||
explain_reason({restricted_shell_bad_return, V}, exit=Cl, [], PF, Str) -> | |||||
String = <<"restricted shell module returned bad value ">>, | |||||
format_value(V, String, Cl, PF, Str); | |||||
explain_reason({restricted_shell_disallowed,{ForMF,As}}, | |||||
exit=Cl, [], PF, Str) -> | |||||
%% ForMF can be a fun, but not a shell fun. | |||||
String = <<"restricted shell does not allow ">>, | |||||
format_errstr_call(String, Cl, ForMF, As, PF, Str); | |||||
explain_reason(restricted_shell_started, exit, [], _PF, _Str) -> | |||||
<<"restricted shell starts now">>; | |||||
explain_reason(restricted_shell_stopped, exit, [], _PF, _Str) -> | |||||
<<"restricted shell stopped">>; | |||||
%% Other exit code: | |||||
explain_reason(Reason, Class, [], PF, Str) -> | |||||
PF(Reason, (iolist_size(Str)+1) + exited_size(Class)). | |||||
n_spaces(N) -> | |||||
lists:duplicate(N, $\s). | |||||
exited_size(Class) -> | |||||
iolist_size(exited(Class)). | |||||
exited(error) -> | |||||
<<"exception error: ">>; | |||||
exited(exit) -> | |||||
<<"exception exit: ">>; | |||||
exited(throw) -> | |||||
<<"exception throw: ">>. | |||||
format_stacktrace1(S0, Stack0, PF, SF) -> | |||||
Stack1 = lists:dropwhile(fun({M,F,A}) -> SF(M, F, A) | |||||
end, lists:reverse(Stack0)), | |||||
S = [" " | S0], | |||||
Stack = lists:reverse(Stack1), | |||||
format_stacktrace2(S, Stack, 1, PF). | |||||
format_stacktrace2(S, [{M,F,A}|Fs], N, PF) when is_integer(A) -> | |||||
[io_lib:fwrite(<<"~s~s ~s">>, | |||||
[sep(N, S), origin(N, M, F, A), mfa_to_string(M, F, A)]) | |||||
| format_stacktrace2(S, Fs, N + 1, PF)]; | |||||
format_stacktrace2(S, [{M,F,As}|Fs], N, PF) when is_list(As) -> | |||||
A = length(As), | |||||
CalledAs = [S,<<" called as ">>], | |||||
C = format_call("", CalledAs, {M,F}, As, PF), | |||||
[io_lib:fwrite(<<"~s~s ~s\n~s~s">>, | |||||
[sep(N, S), origin(N, M, F, A), mfa_to_string(M, F, A), | |||||
CalledAs, C]) | |||||
| format_stacktrace2(S, Fs, N + 1, PF)]; | |||||
format_stacktrace2(_S, [], _N, _PF) -> | |||||
"". | |||||
argss(0) -> | |||||
<<"no arguments">>; | |||||
argss(1) -> | |||||
<<"one argument">>; | |||||
argss(2) -> | |||||
<<"two arguments">>; | |||||
argss(I) -> | |||||
io_lib:fwrite(<<"~w arguments">>, [I]). | |||||
format_value(V, ErrStr, Class, PF, Str) -> | |||||
Pre1Sz = exited_size(Class), | |||||
Str1 = PF(V, Pre1Sz + iolist_size([Str, ErrStr])+1), | |||||
[ErrStr | case count_nl(Str1) of | |||||
N1 when N1 > 1 -> | |||||
Str2 = PF(V, iolist_size(Str) + 1 + Pre1Sz), | |||||
case count_nl(Str2) < N1 of | |||||
true -> | |||||
[$\n, Str, n_spaces(Pre1Sz) | Str2]; | |||||
false -> | |||||
Str1 | |||||
end; | |||||
_ -> | |||||
Str1 | |||||
end]. | |||||
format_fun(Fun) when is_function(Fun) -> | |||||
{module, M} = erlang:fun_info(Fun, module), | |||||
{name, F} = erlang:fun_info(Fun, name), | |||||
{arity, A} = erlang:fun_info(Fun, arity), | |||||
case erlang:fun_info(Fun, type) of | |||||
{type, local} when F =:= "" -> | |||||
io_lib:fwrite(<<"~w">>, [Fun]); | |||||
{type, local} when M =:= erl_eval -> | |||||
io_lib:fwrite(<<"interpreted function with arity ~w">>, [A]); | |||||
{type, local} -> | |||||
mfa_to_string(M, F, A); | |||||
{type, external} -> | |||||
mfa_to_string(M, F, A) | |||||
end. | |||||
format_errstr_call(ErrStr, Class, ForMForFun, As, PF, Pre0) -> | |||||
Pre1 = [Pre0 | n_spaces(exited_size(Class))], | |||||
format_call(ErrStr, Pre1, ForMForFun, As, PF). | |||||
format_call(ErrStr, Pre1, ForMForFun, As, PF) -> | |||||
Arity = length(As), | |||||
[ErrStr | | |||||
case is_op(ForMForFun, Arity) of | |||||
{yes,Op} -> | |||||
format_op(ErrStr, Pre1, Op, As, PF); | |||||
no -> | |||||
MFs = mf_to_string(ForMForFun, Arity), | |||||
I1 = iolist_size([Pre1,ErrStr|MFs]), | |||||
S1 = pp_arguments(PF, As, I1), | |||||
S2 = pp_arguments(PF, As, iolist_size([Pre1|MFs])), | |||||
Long = count_nl(pp_arguments(PF, [a2345,b2345], I1)) > 0, | |||||
case Long or (count_nl(S2) < count_nl(S1)) of | |||||
true -> | |||||
[$\n, Pre1, MFs, S2]; | |||||
false -> | |||||
[MFs, S1] | |||||
end | |||||
end]. | |||||
mfa_to_string(M, F, A) -> | |||||
io_lib:fwrite(<<"~s/~w">>, [mf_to_string({M, F}, A), A]). | |||||
mf_to_string({M, F}, A) -> | |||||
case erl_internal:bif(M, F, A) of | |||||
true -> | |||||
io_lib:fwrite(<<"~w">>, [F]); | |||||
false -> | |||||
case is_op({M, F}, A) of | |||||
{yes, '/'} -> | |||||
io_lib:fwrite(<<"~w">>, [F]); | |||||
{yes, F} -> | |||||
atom_to_list(F); | |||||
no -> | |||||
io_lib:fwrite(<<"~w:~w">>, [M, F]) | |||||
end | |||||
end; | |||||
mf_to_string(Fun, _A) when is_function(Fun) -> | |||||
format_fun(Fun); | |||||
mf_to_string(F, _A) -> | |||||
io_lib:fwrite(<<"~w">>, [F]). | |||||
n_args(A) when is_integer(A) -> | |||||
A; | |||||
n_args(As) when is_list(As) -> | |||||
length(As). | |||||
origin(1, M, F, A) -> | |||||
case is_op({M, F}, n_args(A)) of | |||||
{yes, F} -> <<"in operator ">>; | |||||
no -> <<"in function ">> | |||||
end; | |||||
origin(_N, _M, _F, _A) -> | |||||
<<"in call from">>. | |||||
sep(1, S) -> S; | |||||
sep(_, S) -> [$\n | S]. | |||||
count_nl([E | Es]) -> | |||||
count_nl(E) + count_nl(Es); | |||||
count_nl($\n) -> | |||||
1; | |||||
count_nl(Bin) when is_binary(Bin) -> | |||||
count_nl(binary_to_list(Bin)); | |||||
count_nl(_) -> | |||||
0. | |||||
is_op(ForMForFun, A) -> | |||||
try | |||||
{erlang,F} = ForMForFun, | |||||
_ = erl_internal:op_type(F, A), | |||||
{yes,F} | |||||
catch error:_ -> no | |||||
end. | |||||
format_op(ErrStr, Pre, Op, [A1, A2], PF) -> | |||||
I1 = iolist_size([ErrStr,Pre]), | |||||
S1 = PF(A1, I1+1), | |||||
S2 = PF(A2, I1+1), | |||||
OpS = atom_to_list(Op), | |||||
Pre1 = [$\n | n_spaces(I1)], | |||||
case count_nl(S1) > 0 of | |||||
true -> | |||||
[S1,Pre1,OpS,Pre1|S2]; | |||||
false -> | |||||
OpS2 = io_lib:fwrite(<<" ~s ">>, [Op]), | |||||
S2_2 = PF(A2, iolist_size([ErrStr,Pre,S1|OpS2])+1), | |||||
case count_nl(S2) < count_nl(S2_2) of | |||||
true -> | |||||
[S1,Pre1,OpS,Pre1|S2]; | |||||
false -> | |||||
[S1,OpS2|S2_2] | |||||
end | |||||
end. | |||||
pp_arguments(PF, As, I) -> | |||||
case {As, io_lib:printable_list(As)} of | |||||
{[Int | T], true} -> | |||||
L = integer_to_list(Int), | |||||
Ll = length(L), | |||||
A = list_to_atom(lists:duplicate(Ll, $a)), | |||||
S0 = binary_to_list(iolist_to_binary(PF([A | T], I+1))), | |||||
brackets_to_parens([$[,L,string:sub_string(S0, 2+Ll)]); | |||||
_ -> | |||||
brackets_to_parens(PF(As, I+1)) | |||||
end. | |||||
brackets_to_parens(S) -> | |||||
B = iolist_to_binary(S), | |||||
Sz = byte_size(B) - 2, | |||||
<<$[,R:Sz/binary,$]>> = B, | |||||
[$(,R,$)]. | |||||
@ -0,0 +1,92 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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's top level supervisor. | |||||
%% @private | |||||
-module(lager_sup). | |||||
-behaviour(supervisor). | |||||
%% API | |||||
-export([start_link/0]). | |||||
%% Callbacks | |||||
-export([init/1]). | |||||
start_link() -> | |||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []). | |||||
init([]) -> | |||||
%% set up the config, is safe even during relups | |||||
lager_config:new(), | |||||
%% TODO: | |||||
%% Always start lager_event as the default and make sure that | |||||
%% other gen_event stuff can start up as needed | |||||
%% | |||||
%% Maybe a new API to handle the sink and its policy? | |||||
Children = [ | |||||
{lager, {gen_event, start_link, [{local, lager_event}]}, | |||||
permanent, 5000, worker, dynamic}, | |||||
{lager_handler_watcher_sup, {lager_handler_watcher_sup, start_link, []}, | |||||
permanent, 5000, supervisor, [lager_handler_watcher_sup]}], | |||||
CrashLog = decide_crash_log(application:get_env(lager, crash_log, false)), | |||||
{ok, {{one_for_one, 10, 60}, | |||||
Children ++ CrashLog | |||||
}}. | |||||
validate_positive({ok, Val}, _Default) when is_integer(Val) andalso Val >= 0 -> | |||||
Val; | |||||
validate_positive(_Val, Default) -> | |||||
Default. | |||||
determine_rotation_date({ok, ""}) -> | |||||
undefined; | |||||
determine_rotation_date({ok, Val3}) -> | |||||
case lager_util:parse_rotation_date_spec(Val3) of | |||||
{ok, Spec} -> Spec; | |||||
{error, _} -> | |||||
error_logger:error_msg("Invalid date spec for " | |||||
"crash log ~p~n", [Val3]), | |||||
undefined | |||||
end; | |||||
determine_rotation_date(_) -> | |||||
undefined. | |||||
determine_rotator_mod({ok, Mod}, _Default) when is_atom(Mod) -> | |||||
Mod; | |||||
determine_rotator_mod(_, Default) -> | |||||
Default. | |||||
decide_crash_log(undefined) -> | |||||
[]; | |||||
decide_crash_log(false) -> | |||||
[]; | |||||
decide_crash_log(File) -> | |||||
MaxBytes = validate_positive(application:get_env(lager, crash_log_msg_size), 65536), | |||||
RotationSize = validate_positive(application:get_env(lager, crash_log_size), 0), | |||||
RotationCount = validate_positive(application:get_env(lager, crash_log_count), 0), | |||||
RotationDate = determine_rotation_date(application:get_env(lager, crash_log_date)), | |||||
RotationMod = determine_rotator_mod(application:get_env(lager, crash_log_rotator), lager_rotator_default), | |||||
[{lager_crash_log, {lager_crash_log, start_link, [File, MaxBytes, | |||||
RotationSize, RotationDate, RotationCount, RotationMod]}, | |||||
permanent, 5000, worker, [lager_crash_log]}]. |
@ -0,0 +1,339 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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 The parse transform used for lager messages. | |||||
%% This parse transform rewrites functions calls to lager:Severity/1,2 into | |||||
%% a more complicated function that captures module, function, line, pid and | |||||
%% time as well. The entire function call is then wrapped in a case that | |||||
%% checks the lager_config 'loglevel' value, so the code isn't executed if | |||||
%% nothing wishes to consume the message. | |||||
-module(lager_transform). | |||||
-include("lager.hrl"). | |||||
-export([parse_transform/2]). | |||||
%% @private | |||||
parse_transform(AST, Options) -> | |||||
TruncSize = proplists:get_value(lager_truncation_size, Options, ?DEFAULT_TRUNCATION), | |||||
Enable = proplists:get_value(lager_print_records_flag, Options, true), | |||||
Sinks = [lager] ++ proplists:get_value(lager_extra_sinks, Options, []), | |||||
Functions = proplists:get_value(lager_function_transforms, Options, []), | |||||
put(print_records_flag, Enable), | |||||
put(truncation_size, TruncSize), | |||||
put(sinks, Sinks), | |||||
put(functions, lists:keysort(1, Functions)), | |||||
erlang:put(records, []), | |||||
%% .app file should either be in the outdir, or the same dir as the source file | |||||
guess_application(proplists:get_value(outdir, Options), hd(AST)), | |||||
walk_ast([], AST). | |||||
walk_ast(Acc, []) -> | |||||
case get(print_records_flag) of | |||||
true -> | |||||
insert_record_attribute(Acc); | |||||
false -> | |||||
lists:reverse(Acc) | |||||
end; | |||||
walk_ast(Acc, [{attribute, _, module, {Module, _PmodArgs}}=H|T]) -> | |||||
%% A wild parameterized module appears! | |||||
put(module, Module), | |||||
walk_ast([H|Acc], T); | |||||
walk_ast(Acc, [{attribute, _, module, Module}=H|T]) -> | |||||
put(module, Module), | |||||
walk_ast([H|Acc], T); | |||||
walk_ast(Acc, [{attribute, _, lager_function_transforms, FromModule }=H|T]) -> | |||||
%% Merge transform options from the module over the compile options | |||||
FromOptions = get(functions), | |||||
put(functions, orddict:merge(fun(_Key, _V1, V2) -> V2 end, FromOptions, lists:keysort(1, FromModule))), | |||||
walk_ast([H|Acc], T); | |||||
walk_ast(Acc, [{function, Line, Name, Arity, Clauses}|T]) -> | |||||
put(function, Name), | |||||
walk_ast([{function, Line, Name, Arity, | |||||
walk_clauses([], Clauses)}|Acc], T); | |||||
walk_ast(Acc, [{attribute, _, record, {Name, Fields}}=H|T]) -> | |||||
FieldNames = lists:map(fun record_field_name/1, Fields), | |||||
stash_record({Name, FieldNames}), | |||||
walk_ast([H|Acc], T); | |||||
walk_ast(Acc, [H|T]) -> | |||||
walk_ast([H|Acc], T). | |||||
record_field_name({record_field, _, {atom, _, FieldName}}) -> | |||||
FieldName; | |||||
record_field_name({record_field, _, {atom, _, FieldName}, _Default}) -> | |||||
FieldName; | |||||
record_field_name({typed_record_field, Field, _Type}) -> | |||||
record_field_name(Field). | |||||
walk_clauses(Acc, []) -> | |||||
lists:reverse(Acc); | |||||
walk_clauses(Acc, [{clause, Line, Arguments, Guards, Body}|T]) -> | |||||
walk_clauses([{clause, Line, Arguments, Guards, walk_body([], Body)}|Acc], T). | |||||
walk_body(Acc, []) -> | |||||
lists:reverse(Acc); | |||||
walk_body(Acc, [H|T]) -> | |||||
walk_body([transform_statement(H, get(sinks))|Acc], T). | |||||
transform_statement({call, Line, {remote, _Line1, {atom, _Line2, Module}, | |||||
{atom, _Line3, Function}}, Arguments0} = Stmt, | |||||
Sinks) -> | |||||
case lists:member(Module, Sinks) of | |||||
true -> | |||||
case lists:member(Function, ?LEVELS) of | |||||
true -> | |||||
SinkName = lager_util:make_internal_sink_name(Module), | |||||
do_transform(Line, SinkName, Function, Arguments0); | |||||
false -> | |||||
case lists:keyfind(Function, 1, ?LEVELS_UNSAFE) of | |||||
{Function, Severity} -> | |||||
SinkName = lager_util:make_internal_sink_name(Module), | |||||
do_transform(Line, SinkName, Severity, Arguments0, unsafe); | |||||
false -> | |||||
Stmt | |||||
end | |||||
end; | |||||
false -> | |||||
list_to_tuple(transform_statement(tuple_to_list(Stmt), Sinks)) | |||||
end; | |||||
transform_statement(Stmt, Sinks) when is_tuple(Stmt) -> | |||||
list_to_tuple(transform_statement(tuple_to_list(Stmt), Sinks)); | |||||
transform_statement(Stmt, Sinks) when is_list(Stmt) -> | |||||
[transform_statement(S, Sinks) || S <- Stmt]; | |||||
transform_statement(Stmt, _Sinks) -> | |||||
Stmt. | |||||
add_function_transforms(_Line, DefaultAttrs, []) -> | |||||
DefaultAttrs; | |||||
add_function_transforms(Line, DefaultAttrs, [{Atom, on_emit, {Module, Function}}|Remainder]) -> | |||||
NewFunction = {tuple, Line, [ | |||||
{atom, Line, Atom}, | |||||
{'fun', Line, { | |||||
function, {atom, Line, Module}, {atom, Line, Function}, {integer, Line, 0} | |||||
}} | |||||
]}, | |||||
add_function_transforms(Line, {cons, Line, NewFunction, DefaultAttrs}, Remainder); | |||||
add_function_transforms(Line, DefaultAttrs, [{Atom, on_log, {Module, Function}}|Remainder]) -> | |||||
NewFunction = {tuple, Line, [ | |||||
{atom, Line, Atom}, | |||||
{call, Line, {remote, Line, {atom, Line, Module}, {atom, Line, Function}}, []} | |||||
]}, | |||||
add_function_transforms(Line, {cons, Line, NewFunction, DefaultAttrs}, Remainder). | |||||
do_transform(Line, SinkName, Severity, Arguments0) -> | |||||
do_transform(Line, SinkName, Severity, Arguments0, safe). | |||||
do_transform(Line, SinkName, Severity, Arguments0, Safety) -> | |||||
SeverityAsInt=lager_util:level_to_num(Severity), | |||||
DefaultAttrs0 = {cons, Line, {tuple, Line, [ | |||||
{atom, Line, module}, {atom, Line, get(module)}]}, | |||||
{cons, Line, {tuple, Line, [ | |||||
{atom, Line, function}, {atom, Line, get(function)}]}, | |||||
{cons, Line, {tuple, Line, [ | |||||
{atom, Line, line}, | |||||
{integer, Line, Line}]}, | |||||
{cons, Line, {tuple, Line, [ | |||||
{atom, Line, pid}, | |||||
{call, Line, {atom, Line, pid_to_list}, [ | |||||
{call, Line, {atom, Line ,self}, []}]}]}, | |||||
{cons, Line, {tuple, Line, [ | |||||
{atom, Line, node}, | |||||
{call, Line, {atom, Line, node}, []}]}, | |||||
%% get the metadata with lager:md(), this will always return a list so we can use it as the tail here | |||||
{call, Line, {remote, Line, {atom, Line, lager}, {atom, Line, md}}, []}}}}}}, | |||||
%{nil, Line}}}}}}}, | |||||
Functions = get(functions), | |||||
DefaultAttrs1 = add_function_transforms(Line, DefaultAttrs0, Functions), | |||||
DefaultAttrs = case erlang:get(application) of | |||||
undefined -> | |||||
DefaultAttrs1; | |||||
App -> | |||||
%% stick the application in the attribute list | |||||
concat_lists({cons, Line, {tuple, Line, [ | |||||
{atom, Line, application}, | |||||
{atom, Line, App}]}, | |||||
{nil, Line}}, DefaultAttrs1) | |||||
end, | |||||
{Meta, Message, Arguments} = handle_args(DefaultAttrs, Line, Arguments0), | |||||
%% Generate some unique variable names so we don't accidentally export from case clauses. | |||||
%% Note that these are not actual atoms, but the AST treats variable names as atoms. | |||||
LevelVar = make_varname("__Level", Line), | |||||
TracesVar = make_varname("__Traces", Line), | |||||
PidVar = make_varname("__Pid", Line), | |||||
LogFun = case Safety of | |||||
safe -> | |||||
do_log; | |||||
unsafe -> | |||||
do_log_unsafe | |||||
end, | |||||
%% Wrap the call to lager:dispatch_log/6 in case that will avoid doing any work if this message is not elegible for logging | |||||
%% See lager.erl (lines 89-100) for lager:dispatch_log/6 | |||||
%% case {whereis(Sink), whereis(?DEFAULT_SINK), lager_config:get({Sink, loglevel}, {?LOG_NONE, []})} of | |||||
{'case',Line, | |||||
{tuple,Line, | |||||
[{call,Line,{atom,Line,whereis},[{atom,Line,SinkName}]}, | |||||
{call,Line,{atom,Line,whereis},[{atom,Line,?DEFAULT_SINK}]}, | |||||
{call,Line, | |||||
{remote,Line,{atom,Line,lager_config},{atom,Line,get}}, | |||||
[{tuple,Line,[{atom,Line,SinkName},{atom,Line,loglevel}]}, | |||||
{tuple,Line,[{integer,Line,0},{nil,Line}]}]}]}, | |||||
%% {undefined, undefined, _} -> {error, lager_not_running}; | |||||
[{clause,Line, | |||||
[{tuple,Line, | |||||
[{atom,Line,undefined},{atom,Line,undefined},{var,Line,'_'}]}], | |||||
[], | |||||
%% trick the linter into avoiding a 'term constructed but not used' error: | |||||
%% (fun() -> {error, lager_not_running} end)() | |||||
[{call, Line, {'fun', Line, {clauses, [{clause, Line, [],[], [{tuple, Line, [{atom, Line, error},{atom, Line, lager_not_running}]}]}]}}, []}] | |||||
}, | |||||
%% {undefined, _, _} -> {error, {sink_not_configured, Sink}}; | |||||
{clause,Line, | |||||
[{tuple,Line, | |||||
[{atom,Line,undefined},{var,Line,'_'},{var,Line,'_'}]}], | |||||
[], | |||||
%% same trick as above to avoid linter error | |||||
[{call, Line, {'fun', Line, {clauses, [{clause, Line, [],[], [{tuple,Line, [{atom,Line,error}, {tuple,Line,[{atom,Line,sink_not_configured},{atom,Line,SinkName}]}]}]}]}}, []}] | |||||
}, | |||||
%% {SinkPid, _, {Level, Traces}} when ... -> lager:do_log/9; | |||||
{clause,Line, | |||||
[{tuple,Line, | |||||
[{var,Line,PidVar}, | |||||
{var,Line,'_'}, | |||||
{tuple,Line,[{var,Line,LevelVar},{var,Line,TracesVar}]}]}], | |||||
[[{op, Line, 'orelse', | |||||
{op, Line, '/=', {op, Line, 'band', {var, Line, LevelVar}, {integer, Line, SeverityAsInt}}, {integer, Line, 0}}, | |||||
{op, Line, '/=', {var, Line, TracesVar}, {nil, Line}}}]], | |||||
[{call,Line,{remote, Line, {atom, Line, lager}, {atom, Line, LogFun}}, | |||||
[{atom,Line,Severity}, | |||||
Meta, | |||||
Message, | |||||
Arguments, | |||||
{integer, Line, get(truncation_size)}, | |||||
{integer, Line, SeverityAsInt}, | |||||
{var, Line, LevelVar}, | |||||
{var, Line, TracesVar}, | |||||
{atom, Line, SinkName}, | |||||
{var, Line, PidVar}]}]}, | |||||
%% _ -> ok | |||||
{clause,Line,[{var,Line,'_'}],[],[{atom,Line,ok}]}]}. | |||||
handle_args(DefaultAttrs, Line, [{cons, LineNum, {tuple, _, _}, _} = Attrs]) -> | |||||
{concat_lists(DefaultAttrs, Attrs), {string, LineNum, ""}, {atom, Line, none}}; | |||||
handle_args(DefaultAttrs, Line, [Format]) -> | |||||
{DefaultAttrs, Format, {atom, Line, none}}; | |||||
handle_args(DefaultAttrs, Line, [Arg1, Arg2]) -> | |||||
%% some ambiguity here, figure out if these arguments are | |||||
%% [Format, Args] or [Attr, Format]. | |||||
%% The trace attributes will be a list of tuples, so check | |||||
%% for that. | |||||
case {element(1, Arg1), Arg1} of | |||||
{_, {cons, _, {tuple, _, _}, _}} -> | |||||
{concat_lists(Arg1, DefaultAttrs), | |||||
Arg2, {atom, Line, none}}; | |||||
{Type, _} when Type == var; | |||||
Type == lc; | |||||
Type == call; | |||||
Type == record_field -> | |||||
%% crap, its not a literal. look at the second | |||||
%% argument to see if it is a string | |||||
case Arg2 of | |||||
{string, _, _} -> | |||||
{concat_lists(Arg1, DefaultAttrs), | |||||
Arg2, {atom, Line, none}}; | |||||
_ -> | |||||
%% not a string, going to have to guess | |||||
%% it's the argument list | |||||
{DefaultAttrs, Arg1, Arg2} | |||||
end; | |||||
_ -> | |||||
{DefaultAttrs, Arg1, Arg2} | |||||
end; | |||||
handle_args(DefaultAttrs, _Line, [Attrs, Format, Args]) -> | |||||
{concat_lists(Attrs, DefaultAttrs), Format, Args}. | |||||
make_varname(Prefix, Line) -> | |||||
list_to_atom(Prefix ++ atom_to_list(get(module)) ++ integer_to_list(Line)). | |||||
%% concat 2 list ASTs by replacing the terminating [] in A with the contents of B | |||||
concat_lists({var, Line, _Name}=Var, B) -> | |||||
%% concatenating a var with a cons | |||||
{call, Line, {remote, Line, {atom, Line, lists},{atom, Line, flatten}}, | |||||
[{cons, Line, Var, B}]}; | |||||
concat_lists({lc, Line, _Body, _Generator} = LC, B) -> | |||||
%% concatenating a LC with a cons | |||||
{call, Line, {remote, Line, {atom, Line, lists},{atom, Line, flatten}}, | |||||
[{cons, Line, LC, B}]}; | |||||
concat_lists({call, Line, _Function, _Args} = Call, B) -> | |||||
%% concatenating a call with a cons | |||||
{call, Line, {remote, Line, {atom, Line, lists},{atom, Line, flatten}}, | |||||
[{cons, Line, Call, B}]}; | |||||
concat_lists({record_field, Line, _Var, _Record, _Field} = Rec, B) -> | |||||
%% concatenating a record_field with a cons | |||||
{call, Line, {remote, Line, {atom, Line, lists},{atom, Line, flatten}}, | |||||
[{cons, Line, Rec, B}]}; | |||||
concat_lists({nil, _Line}, B) -> | |||||
B; | |||||
concat_lists({cons, Line, Element, Tail}, B) -> | |||||
{cons, Line, Element, concat_lists(Tail, B)}. | |||||
stash_record(Record) -> | |||||
Records = case erlang:get(records) of | |||||
undefined -> | |||||
[]; | |||||
R -> | |||||
R | |||||
end, | |||||
erlang:put(records, [Record|Records]). | |||||
insert_record_attribute(AST) -> | |||||
lists:foldl(fun({attribute, Line, module, _}=E, Acc) -> | |||||
[E, {attribute, Line, lager_records, erlang:get(records)}|Acc]; | |||||
(E, Acc) -> | |||||
[E|Acc] | |||||
end, [], AST). | |||||
guess_application(Dirname, Attr) when Dirname /= undefined -> | |||||
case find_app_file(Dirname) of | |||||
no_idea -> | |||||
%% try it based on source file directory (app.src most likely) | |||||
guess_application(undefined, Attr); | |||||
_ -> | |||||
ok | |||||
end; | |||||
guess_application(undefined, {attribute, _, file, {Filename, _}}) -> | |||||
Dir = filename:dirname(Filename), | |||||
find_app_file(Dir); | |||||
guess_application(_, _) -> | |||||
ok. | |||||
find_app_file(Dir) -> | |||||
case filelib:wildcard(Dir++"/*.{app,app.src}") of | |||||
[] -> | |||||
no_idea; | |||||
[File] -> | |||||
case file:consult(File) of | |||||
{ok, [{application, Appname, _Attributes}|_]} -> | |||||
erlang:put(application, Appname); | |||||
_ -> | |||||
no_idea | |||||
end; | |||||
_ -> | |||||
%% multiple files, uh oh | |||||
no_idea | |||||
end. |
@ -0,0 +1,878 @@ | |||||
%% ``The contents of this file are subject to the Erlang Public License, | |||||
%% Version 1.1, (the "License"); you may not use this file except in | |||||
%% compliance with the License. You should have received a copy of the | |||||
%% Erlang Public License along with your Erlang distribution. If not, it can be | |||||
%% retrieved via the world wide web at http://www.erlang.org/. | |||||
%% | |||||
%% Software distributed under the License is distributed on an "AS IS" | |||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See | |||||
%% the License for the specific language governing rights and limitations | |||||
%% under the License. | |||||
%% | |||||
%% The Initial Developer of the Original Code is Corelatus AB. | |||||
%% Portions created by Corelatus are Copyright 2003, Corelatus | |||||
%% AB. All Rights Reserved.'' | |||||
%% | |||||
%% @doc Module to print out terms for logging. Limits by length rather than depth. | |||||
%% | |||||
%% The resulting string may be slightly larger than the limit; the intention | |||||
%% is to provide predictable CPU and memory consumption for formatting | |||||
%% terms, not produce precise string lengths. | |||||
%% | |||||
%% Typical use: | |||||
%% | |||||
%% trunc_io:print(Term, 500). | |||||
%% | |||||
%% Source license: Erlang Public License. | |||||
%% Original author: Matthias Lang, <tt>matthias@corelatus.se</tt> | |||||
%% | |||||
%% Various changes to this module, most notably the format/3 implementation | |||||
%% were added by Andrew Thompson `<andrew@basho.com>'. The module has been renamed | |||||
%% to avoid conflicts with the vanilla module. | |||||
-module(lager_trunc_io). | |||||
-author('matthias@corelatus.se'). | |||||
%% And thanks to Chris Newcombe for a bug fix | |||||
-export([format/3, format/4, print/2, print/3, fprint/2, fprint/3, safe/2]). % interface functions | |||||
-version("$Id: trunc_io.erl,v 1.11 2009-02-23 12:01:06 matthias Exp $"). | |||||
-ifdef(TEST). | |||||
-export([perf/0, perf/3, perf1/0, test/0, test/2]). % testing functions | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
-type option() :: {'depth', integer()} | |||||
| {'lists_as_strings', boolean()} | |||||
| {'force_strings', boolean()}. | |||||
-type options() :: [option()]. | |||||
-record(print_options, { | |||||
%% negative depth means no depth limiting | |||||
depth = -1 :: integer(), | |||||
%% whether to print lists as strings, if possible | |||||
lists_as_strings = true :: boolean(), | |||||
%% force strings, or binaries to be printed as a string, | |||||
%% even if they're not printable | |||||
force_strings = false :: boolean() | |||||
}). | |||||
format(Fmt, Args, Max) -> | |||||
format(Fmt, Args, Max, []). | |||||
format(Fmt, Args, Max, Options) -> | |||||
try lager_format:format(Fmt, Args, Max, Options) | |||||
catch | |||||
_What:_Why -> | |||||
erlang:error(badarg, [Fmt, Args]) | |||||
end. | |||||
%% @doc Returns an flattened list containing the ASCII representation of the given | |||||
%% term. | |||||
-spec fprint(term(), pos_integer()) -> string(). | |||||
fprint(Term, Max) -> | |||||
fprint(Term, Max, []). | |||||
%% @doc Returns an flattened list containing the ASCII representation of the given | |||||
%% term. | |||||
-spec fprint(term(), pos_integer(), options()) -> string(). | |||||
fprint(T, Max, Options) -> | |||||
{L, _} = print(T, Max, prepare_options(Options, #print_options{})), | |||||
lists:flatten(L). | |||||
%% @doc Same as print, but never crashes. | |||||
%% | |||||
%% This is a tradeoff. Print might conceivably crash if it's asked to | |||||
%% print something it doesn't understand, for example some new data | |||||
%% type in a future version of Erlang. If print crashes, we fall back | |||||
%% to io_lib to format the term, but then the formatting is | |||||
%% depth-limited instead of length limited, so you might run out | |||||
%% memory printing it. Out of the frying pan and into the fire. | |||||
%% | |||||
-spec safe(term(), pos_integer()) -> {string(), pos_integer()} | {string()}. | |||||
safe(What, Len) -> | |||||
case catch print(What, Len) of | |||||
{L, Used} when is_list(L) -> {L, Used}; | |||||
_ -> {"unable to print" ++ io_lib:write(What, 99)} | |||||
end. | |||||
%% @doc Returns {List, Length} | |||||
-spec print(term(), pos_integer()) -> {iolist(), pos_integer()}. | |||||
print(Term, Max) -> | |||||
print(Term, Max, []). | |||||
%% @doc Returns {List, Length} | |||||
-spec print(term(), pos_integer(), options() | #print_options{}) -> {iolist(), pos_integer()}. | |||||
print(Term, Max, Options) when is_list(Options) -> | |||||
%% need to convert the proplist to a record | |||||
print(Term, Max, prepare_options(Options, #print_options{})); | |||||
print(Term, _Max, #print_options{force_strings=true}) when not is_list(Term), not is_binary(Term), not is_atom(Term) -> | |||||
erlang:error(badarg); | |||||
print(_, Max, _Options) when Max < 0 -> {"...", 3}; | |||||
print(_, _, #print_options{depth=0}) -> {"...", 3}; | |||||
%% @doc We assume atoms, floats, funs, integers, PIDs, ports and refs never need | |||||
%% to be truncated. This isn't strictly true, someone could make an | |||||
%% arbitrarily long bignum. Let's assume that won't happen unless someone | |||||
%% is being malicious. | |||||
%% | |||||
print(Atom, _Max, #print_options{force_strings=NoQuote}) when is_atom(Atom) -> | |||||
L = atom_to_list(Atom), | |||||
R = case atom_needs_quoting_start(L) andalso not NoQuote of | |||||
true -> lists:flatten([$', L, $']); | |||||
false -> L | |||||
end, | |||||
{R, length(R)}; | |||||
print(<<>>, _Max, #print_options{depth=1}) -> | |||||
{"<<>>", 4}; | |||||
print(Bin, _Max, #print_options{depth=1}) when is_binary(Bin) -> | |||||
{"<<...>>", 7}; | |||||
print(<<>>, _Max, Options) -> | |||||
case Options#print_options.force_strings of | |||||
true -> | |||||
{"", 0}; | |||||
false -> | |||||
{"<<>>", 4} | |||||
end; | |||||
print(Binary, 0, _Options) when is_bitstring(Binary) -> | |||||
{"<<..>>", 6}; | |||||
print(Bin, Max, _Options) when is_binary(Bin), Max < 2 -> | |||||
{"<<...>>", 7}; | |||||
print(Binary, Max, Options) when is_binary(Binary) -> | |||||
B = binary_to_list(Binary, 1, lists:min([Max, byte_size(Binary)])), | |||||
{Res, Length} = case Options#print_options.lists_as_strings orelse | |||||
Options#print_options.force_strings of | |||||
true -> | |||||
Depth = Options#print_options.depth, | |||||
MaxSize = (Depth - 1) * 4, | |||||
%% check if we need to truncate based on depth | |||||
In = case Depth > -1 andalso MaxSize < length(B) andalso | |||||
not Options#print_options.force_strings of | |||||
true -> | |||||
string:substr(B, 1, MaxSize); | |||||
false -> B | |||||
end, | |||||
MaxLen = case Options#print_options.force_strings of | |||||
true -> | |||||
Max; | |||||
false -> | |||||
%% make room for the leading doublequote | |||||
Max - 1 | |||||
end, | |||||
try alist(In, MaxLen, Options) of | |||||
{L0, Len0} -> | |||||
case Options#print_options.force_strings of | |||||
false -> | |||||
case B /= In of | |||||
true -> | |||||
{[$", L0, "..."], Len0+4}; | |||||
false -> | |||||
{[$"|L0], Len0+1} | |||||
end; | |||||
true -> | |||||
{L0, Len0} | |||||
end | |||||
catch | |||||
throw:{unprintable, C} -> | |||||
Index = string:chr(In, C), | |||||
case Index > 1 andalso Options#print_options.depth =< Index andalso | |||||
Options#print_options.depth > -1 andalso | |||||
not Options#print_options.force_strings of | |||||
true -> | |||||
%% print first Index-1 characters followed by ... | |||||
{L0, Len0} = alist_start(string:substr(In, 1, Index - 1), Max - 1, Options), | |||||
{L0++"...", Len0+3}; | |||||
false -> | |||||
list_body(In, Max-4, dec_depth(Options), true) | |||||
end | |||||
end; | |||||
_ -> | |||||
list_body(B, Max-4, dec_depth(Options), true) | |||||
end, | |||||
case Options#print_options.force_strings of | |||||
true -> | |||||
{Res, Length}; | |||||
_ -> | |||||
{["<<", Res, ">>"], Length+4} | |||||
end; | |||||
%% bitstrings are binary's evil brother who doesn't end on an 8 bit boundary. | |||||
%% This makes printing them extremely annoying, so list_body/list_bodyc has | |||||
%% some magic for dealing with the output of bitstring_to_list, which returns | |||||
%% a list of integers (as expected) but with a trailing binary that represents | |||||
%% the remaining bits. | |||||
print({inline_bitstring, B}, _Max, _Options) when is_bitstring(B) -> | |||||
Size = bit_size(B), | |||||
<<Value:Size>> = B, | |||||
ValueStr = integer_to_list(Value), | |||||
SizeStr = integer_to_list(Size), | |||||
{[ValueStr, $:, SizeStr], length(ValueStr) + length(SizeStr) +1}; | |||||
print(BitString, Max, Options) when is_bitstring(BitString) -> | |||||
BL = case byte_size(BitString) > Max of | |||||
true -> | |||||
binary_to_list(BitString, 1, Max); | |||||
_ -> | |||||
R = erlang:bitstring_to_list(BitString), | |||||
{Bytes, [Bits]} = lists:splitwith(fun erlang:is_integer/1, R), | |||||
%% tag the trailing bits with a special tuple we catch when | |||||
%% list_body calls print again | |||||
Bytes ++ [{inline_bitstring, Bits}] | |||||
end, | |||||
{X, Len0} = list_body(BL, Max - 4, dec_depth(Options), true), | |||||
{["<<", X, ">>"], Len0 + 4}; | |||||
print(Float, _Max, _Options) when is_float(Float) -> | |||||
%% use the same function io_lib:format uses to print floats | |||||
%% float_to_list is way too verbose. | |||||
L = io_lib_format:fwrite_g(Float), | |||||
{L, length(L)}; | |||||
print(Fun, Max, _Options) when is_function(Fun) -> | |||||
L = erlang:fun_to_list(Fun), | |||||
case length(L) > Max of | |||||
true -> | |||||
S = erlang:max(5, Max), | |||||
Res = string:substr(L, 1, S) ++ "..>", | |||||
{Res, length(Res)}; | |||||
_ -> | |||||
{L, length(L)} | |||||
end; | |||||
print(Integer, _Max, _Options) when is_integer(Integer) -> | |||||
L = integer_to_list(Integer), | |||||
{L, length(L)}; | |||||
print(Pid, _Max, _Options) when is_pid(Pid) -> | |||||
L = pid_to_list(Pid), | |||||
{L, length(L)}; | |||||
print(Ref, _Max, _Options) when is_reference(Ref) -> | |||||
L = erlang:ref_to_list(Ref), | |||||
{L, length(L)}; | |||||
print(Port, _Max, _Options) when is_port(Port) -> | |||||
L = erlang:port_to_list(Port), | |||||
{L, length(L)}; | |||||
print({'$lager_record', Name, Fields}, Max, Options) -> | |||||
Leader = "#" ++ atom_to_list(Name) ++ "{", | |||||
{RC, Len} = record_fields(Fields, Max - length(Leader) + 1, dec_depth(Options)), | |||||
{[Leader, RC, "}"], Len + length(Leader) + 1}; | |||||
print(Tuple, Max, Options) when is_tuple(Tuple) -> | |||||
{TC, Len} = tuple_contents(Tuple, Max-2, Options), | |||||
{[${, TC, $}], Len + 2}; | |||||
print(List, Max, Options) when is_list(List) -> | |||||
case Options#print_options.lists_as_strings orelse | |||||
Options#print_options.force_strings of | |||||
true -> | |||||
alist_start(List, Max, dec_depth(Options)); | |||||
_ -> | |||||
{R, Len} = list_body(List, Max - 2, dec_depth(Options), false), | |||||
{[$[, R, $]], Len + 2} | |||||
end; | |||||
print(Map, Max, Options) when is_map(Map) -> | |||||
{MapBody, Len} = map_body(Map, Max - 3, dec_depth(Options)), | |||||
{[$#, ${, MapBody, $}], Len + 3}; | |||||
print(Term, Max, Options) -> | |||||
error(badarg, [Term, Max, Options]). | |||||
%% Returns {List, Length} | |||||
tuple_contents(Tuple, Max, Options) -> | |||||
L = tuple_to_list(Tuple), | |||||
list_body(L, Max, dec_depth(Options), true). | |||||
%% Format the inside of a list, i.e. do not add a leading [ or trailing ]. | |||||
%% Returns {List, Length} | |||||
list_body([], _Max, _Options, _Tuple) -> {[], 0}; | |||||
list_body(_, Max, _Options, _Tuple) when Max < 4 -> {"...", 3}; | |||||
list_body(_, _Max, #print_options{depth=0}, _Tuple) -> {"...", 3}; | |||||
list_body([H], Max, Options=#print_options{depth=1}, _Tuple) -> | |||||
print(H, Max, Options); | |||||
list_body([H|_], Max, Options=#print_options{depth=1}, Tuple) -> | |||||
{List, Len} = print(H, Max-4, Options), | |||||
Sep = case Tuple of | |||||
true -> $,; | |||||
false -> $| | |||||
end, | |||||
{[List ++ [Sep | "..."]], Len + 4}; | |||||
list_body([H|T], Max, Options, Tuple) -> | |||||
{List, Len} = print(H, Max, Options), | |||||
{Final, FLen} = list_bodyc(T, Max - Len, Options, Tuple), | |||||
{[List|Final], FLen + Len}; | |||||
list_body(X, Max, Options, _Tuple) -> %% improper list | |||||
{List, Len} = print(X, Max - 1, Options), | |||||
{[$|,List], Len + 1}. | |||||
list_bodyc([], _Max, _Options, _Tuple) -> {[], 0}; | |||||
list_bodyc(_, Max, _Options, _Tuple) when Max < 5 -> {",...", 4}; | |||||
list_bodyc(_, _Max, #print_options{depth=1}, true) -> {",...", 4}; | |||||
list_bodyc(_, _Max, #print_options{depth=1}, false) -> {"|...", 4}; | |||||
list_bodyc([H|T], Max, #print_options{depth=Depth} = Options, Tuple) -> | |||||
{List, Len} = print(H, Max, dec_depth(Options)), | |||||
{Final, FLen} = list_bodyc(T, Max - Len - 1, dec_depth(Options), Tuple), | |||||
Sep = case Depth == 1 andalso not Tuple of | |||||
true -> $|; | |||||
_ -> $, | |||||
end, | |||||
{[Sep, List|Final], FLen + Len + 1}; | |||||
list_bodyc(X, Max, Options, _Tuple) -> %% improper list | |||||
{List, Len} = print(X, Max - 1, Options), | |||||
{[$|,List], Len + 1}. | |||||
map_body(Map, Max, #print_options{depth=Depth}) when Max < 4; Depth =:= 0 -> | |||||
case erlang:map_size(Map) of | |||||
0 -> {[], 0}; | |||||
_ -> {"...", 3} | |||||
end; | |||||
map_body(Map, Max, Options) -> | |||||
case maps:to_list(Map) of | |||||
[] -> | |||||
{[], 0}; | |||||
[{Key, Value} | Rest] -> | |||||
{KeyStr, KeyLen} = print(Key, Max - 4, Options), | |||||
DiffLen = KeyLen + 4, | |||||
{ValueStr, ValueLen} = print(Value, Max - DiffLen, Options), | |||||
DiffLen2 = DiffLen + ValueLen, | |||||
{Final, FLen} = map_bodyc(Rest, Max - DiffLen2, dec_depth(Options)), | |||||
{[KeyStr, " => ", ValueStr | Final], DiffLen2 + FLen} | |||||
end. | |||||
map_bodyc([], _Max, _Options) -> | |||||
{[], 0}; | |||||
map_bodyc(_Rest, Max,#print_options{depth=Depth}) when Max < 5; Depth =:= 0 -> | |||||
{",...", 4}; | |||||
map_bodyc([{Key, Value} | Rest], Max, Options) -> | |||||
{KeyStr, KeyLen} = print(Key, Max - 5, Options), | |||||
DiffLen = KeyLen + 5, | |||||
{ValueStr, ValueLen} = print(Value, Max - DiffLen, Options), | |||||
DiffLen2 = DiffLen + ValueLen, | |||||
{Final, FLen} = map_bodyc(Rest, Max - DiffLen2, dec_depth(Options)), | |||||
{[$,, KeyStr, " => ", ValueStr | Final], DiffLen2 + FLen}. | |||||
%% The head of a list we hope is ascii. Examples: | |||||
%% | |||||
%% [65,66,67] -> "ABC" | |||||
%% [65,0,67] -> "A"[0,67] | |||||
%% [0,65,66] -> [0,65,66] | |||||
%% [65,b,66] -> "A"[b,66] | |||||
%% | |||||
alist_start([], _Max, #print_options{force_strings=true}) -> {"", 0}; | |||||
alist_start([], _Max, _Options) -> {"[]", 2}; | |||||
alist_start(_, Max, _Options) when Max < 4 -> {"...", 3}; | |||||
alist_start(_, _Max, #print_options{depth=0}) -> {"[...]", 5}; | |||||
alist_start(L, Max, #print_options{force_strings=true} = Options) -> | |||||
alist(L, Max, Options); | |||||
%alist_start([H|_T], _Max, #print_options{depth=1}) when is_integer(H) -> {[$[, H, $|, $., $., $., $]], 7}; | |||||
alist_start([H|T], Max, Options) when is_integer(H), H >= 16#20, H =< 16#7e -> % definitely printable | |||||
try alist([H|T], Max -1, Options) of | |||||
{L, Len} -> | |||||
{[$"|L], Len + 1} | |||||
catch | |||||
throw:{unprintable, _} -> | |||||
{R, Len} = list_body([H|T], Max-2, Options, false), | |||||
{[$[, R, $]], Len + 2} | |||||
end; | |||||
alist_start([H|T], Max, Options) when is_integer(H), H >= 16#a0, H =< 16#ff -> % definitely printable | |||||
try alist([H|T], Max -1, Options) of | |||||
{L, Len} -> | |||||
{[$"|L], Len + 1} | |||||
catch | |||||
throw:{unprintable, _} -> | |||||
{R, Len} = list_body([H|T], Max-2, Options, false), | |||||
{[$[, R, $]], Len + 2} | |||||
end; | |||||
alist_start([H|T], Max, Options) when H =:= $\t; H =:= $\n; H =:= $\r; H =:= $\v; H =:= $\e; H=:= $\f; H=:= $\b -> | |||||
try alist([H|T], Max -1, Options) of | |||||
{L, Len} -> | |||||
{[$"|L], Len + 1} | |||||
catch | |||||
throw:{unprintable, _} -> | |||||
{R, Len} = list_body([H|T], Max-2, Options, false), | |||||
{[$[, R, $]], Len + 2} | |||||
end; | |||||
alist_start(L, Max, Options) -> | |||||
{R, Len} = list_body(L, Max-2, Options, false), | |||||
{[$[, R, $]], Len + 2}. | |||||
alist([], _Max, #print_options{force_strings=true}) -> {"", 0}; | |||||
alist([], _Max, _Options) -> {"\"", 1}; | |||||
alist(_, Max, #print_options{force_strings=true}) when Max < 4 -> {"...", 3}; | |||||
alist(_, Max, #print_options{force_strings=false}) when Max < 5 -> {"...\"", 4}; | |||||
alist([H|T], Max, Options = #print_options{force_strings=false,lists_as_strings=true}) when H =:= $"; H =:= $\\ -> | |||||
%% preserve escaping around quotes | |||||
{L, Len} = alist(T, Max-1, Options), | |||||
{[$\\,H|L], Len + 2}; | |||||
alist([H|T], Max, Options) when is_integer(H), H >= 16#20, H =< 16#7e -> % definitely printable | |||||
{L, Len} = alist(T, Max-1, Options), | |||||
{[H|L], Len + 1}; | |||||
alist([H|T], Max, Options) when is_integer(H), H >= 16#a0, H =< 16#ff -> % definitely printable | |||||
{L, Len} = alist(T, Max-1, Options), | |||||
{[H|L], Len + 1}; | |||||
alist([H|T], Max, Options) when H =:= $\t; H =:= $\n; H =:= $\r; H =:= $\v; H =:= $\e; H=:= $\f; H=:= $\b -> | |||||
{L, Len} = alist(T, Max-1, Options), | |||||
case Options#print_options.force_strings of | |||||
true -> | |||||
{[H|L], Len + 1}; | |||||
_ -> | |||||
{[escape(H)|L], Len + 1} | |||||
end; | |||||
alist([H|T], Max, #print_options{force_strings=true} = Options) when is_integer(H) -> | |||||
{L, Len} = alist(T, Max-1, Options), | |||||
{[H|L], Len + 1}; | |||||
alist([H|T], Max, Options = #print_options{force_strings=true}) when is_binary(H); is_list(H) -> | |||||
{List, Len} = print(H, Max, Options), | |||||
case (Max - Len) =< 0 of | |||||
true -> | |||||
%% no more room to print anything | |||||
{List, Len}; | |||||
false -> | |||||
%% no need to decrement depth, as we're in printable string mode | |||||
{Final, FLen} = alist(T, Max - Len, Options), | |||||
{[List|Final], FLen+Len} | |||||
end; | |||||
alist(_, _, #print_options{force_strings=true}) -> | |||||
erlang:error(badarg); | |||||
alist([H|_L], _Max, _Options) -> | |||||
throw({unprintable, H}); | |||||
alist(H, _Max, _Options) -> | |||||
%% improper list | |||||
throw({unprintable, H}). | |||||
%% is the first character in the atom alphabetic & lowercase? | |||||
atom_needs_quoting_start([H|T]) when H >= $a, H =< $z -> | |||||
atom_needs_quoting(T); | |||||
atom_needs_quoting_start(_) -> | |||||
true. | |||||
atom_needs_quoting([]) -> | |||||
false; | |||||
atom_needs_quoting([H|T]) when (H >= $a andalso H =< $z); | |||||
(H >= $A andalso H =< $Z); | |||||
(H >= $0 andalso H =< $9); | |||||
H == $@; H == $_ -> | |||||
atom_needs_quoting(T); | |||||
atom_needs_quoting(_) -> | |||||
true. | |||||
-spec prepare_options(options(), #print_options{}) -> #print_options{}. | |||||
prepare_options([], Options) -> | |||||
Options; | |||||
prepare_options([{depth, Depth}|T], Options) when is_integer(Depth) -> | |||||
prepare_options(T, Options#print_options{depth=Depth}); | |||||
prepare_options([{lists_as_strings, Bool}|T], Options) when is_boolean(Bool) -> | |||||
prepare_options(T, Options#print_options{lists_as_strings = Bool}); | |||||
prepare_options([{force_strings, Bool}|T], Options) when is_boolean(Bool) -> | |||||
prepare_options(T, Options#print_options{force_strings = Bool}). | |||||
dec_depth(#print_options{depth=Depth} = Options) when Depth > 0 -> | |||||
Options#print_options{depth=Depth-1}; | |||||
dec_depth(Options) -> | |||||
Options. | |||||
escape($\t) -> "\\t"; | |||||
escape($\n) -> "\\n"; | |||||
escape($\r) -> "\\r"; | |||||
escape($\e) -> "\\e"; | |||||
escape($\f) -> "\\f"; | |||||
escape($\b) -> "\\b"; | |||||
escape($\v) -> "\\v". | |||||
record_fields([], _, _) -> | |||||
{"", 0}; | |||||
record_fields(_, Max, #print_options{depth=D}) when Max < 4; D == 0 -> | |||||
{"...", 3}; | |||||
record_fields([{Field, Value}|T], Max, Options) -> | |||||
{ExtraChars, Terminator} = case T of | |||||
[] -> | |||||
{1, []}; | |||||
_ -> | |||||
{2, ","} | |||||
end, | |||||
{FieldStr, FieldLen} = print(Field, Max - ExtraChars, Options), | |||||
{ValueStr, ValueLen} = print(Value, Max - (FieldLen + ExtraChars), Options), | |||||
{Final, FLen} = record_fields(T, Max - (FieldLen + ValueLen + ExtraChars), dec_depth(Options)), | |||||
{[FieldStr++"="++ValueStr++Terminator|Final], FLen + FieldLen + ValueLen + ExtraChars}. | |||||
-ifdef(TEST). | |||||
%%-------------------- | |||||
%% The start of a test suite. So far, it only checks for not crashing. | |||||
-spec test() -> ok. | |||||
test() -> | |||||
test(trunc_io, print). | |||||
-spec test(atom(), atom()) -> ok. | |||||
test(Mod, Func) -> | |||||
Simple_items = [atom, 1234, 1234.0, {tuple}, [], [list], "string", self(), | |||||
<<1,2,3>>, make_ref(), fun() -> ok end], | |||||
F = fun(A) -> | |||||
Mod:Func(A, 100), | |||||
Mod:Func(A, 2), | |||||
Mod:Func(A, 20) | |||||
end, | |||||
G = fun(A) -> | |||||
case catch F(A) of | |||||
{'EXIT', _} -> exit({failed, A}); | |||||
_ -> ok | |||||
end | |||||
end, | |||||
lists:foreach(G, Simple_items), | |||||
Tuples = [ {1,2,3,a,b,c}, {"abc", def, 1234}, | |||||
{{{{a},b,c,{d},e}},f}], | |||||
Lists = [ [1,2,3,4,5,6,7], lists:seq(1,1000), | |||||
[{a}, {a,b}, {a, [b,c]}, "def"], [a|b], [$a|$b] ], | |||||
lists:foreach(G, Tuples), | |||||
lists:foreach(G, Lists). | |||||
-spec perf() -> ok. | |||||
perf() -> | |||||
{New, _} = timer:tc(trunc_io, perf, [trunc_io, print, 1000]), | |||||
{Old, _} = timer:tc(trunc_io, perf, [io_lib, write, 1000]), | |||||
io:fwrite("New code took ~p us, old code ~p\n", [New, Old]). | |||||
-spec perf(atom(), atom(), integer()) -> done. | |||||
perf(M, F, Reps) when Reps > 0 -> | |||||
test(M,F), | |||||
perf(M,F,Reps-1); | |||||
perf(_,_,_) -> | |||||
done. | |||||
%% Performance test. Needs a particularly large term I saved as a binary... | |||||
-spec perf1() -> {non_neg_integer(), non_neg_integer()}. | |||||
perf1() -> | |||||
{ok, Bin} = file:read_file("bin"), | |||||
A = binary_to_term(Bin), | |||||
{N, _} = timer:tc(trunc_io, print, [A, 1500]), | |||||
{M, _} = timer:tc(io_lib, write, [A]), | |||||
{N, M}. | |||||
format_test() -> | |||||
%% simple format strings | |||||
?assertEqual("foobar", lists:flatten(format("~s", [["foo", $b, $a, $r]], 50))), | |||||
?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~p", [["foo", $b, $a, $r]], 50))), | |||||
?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~P", [["foo", $b, $a, $r], 10], 50))), | |||||
?assertEqual("[[102,111,111],98,97,114]", lists:flatten(format("~w", [["foo", $b, $a, $r]], 50))), | |||||
%% complex ones | |||||
?assertEqual(" foobar", lists:flatten(format("~10s", [["foo", $b, $a, $r]], 50))), | |||||
?assertEqual("f", lists:flatten(format("~1s", [["foo", $b, $a, $r]], 50))), | |||||
?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~22p", [["foo", $b, $a, $r]], 50))), | |||||
?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~22P", [["foo", $b, $a, $r], 10], 50))), | |||||
?assertEqual("**********", lists:flatten(format("~10W", [["foo", $b, $a, $r], 10], 50))), | |||||
?assertEqual("[[102,111,111],98,97,114]", lists:flatten(format("~25W", [["foo", $b, $a, $r], 10], 50))), | |||||
% Note these next two diverge from io_lib:format; the field width is | |||||
% ignored, when it should be used as max line length. | |||||
?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~10p", [["foo", $b, $a, $r]], 50))), | |||||
?assertEqual("[\"foo\",98,97,114]", lists:flatten(format("~10P", [["foo", $b, $a, $r], 10], 50))), | |||||
ok. | |||||
atom_quoting_test() -> | |||||
?assertEqual("hello", lists:flatten(format("~p", [hello], 50))), | |||||
?assertEqual("'hello world'", lists:flatten(format("~p", ['hello world'], 50))), | |||||
?assertEqual("'Hello world'", lists:flatten(format("~p", ['Hello world'], 50))), | |||||
?assertEqual("hello_world", lists:flatten(format("~p", ['hello_world'], 50))), | |||||
?assertEqual("'node@127.0.0.1'", lists:flatten(format("~p", ['node@127.0.0.1'], 50))), | |||||
?assertEqual("node@nohost", lists:flatten(format("~p", [node@nohost], 50))), | |||||
?assertEqual("abc123", lists:flatten(format("~p", [abc123], 50))), | |||||
ok. | |||||
sane_float_printing_test() -> | |||||
?assertEqual("1.0", lists:flatten(format("~p", [1.0], 50))), | |||||
?assertEqual("1.23456789", lists:flatten(format("~p", [1.23456789], 50))), | |||||
?assertEqual("1.23456789", lists:flatten(format("~p", [1.234567890], 50))), | |||||
?assertEqual("0.3333333333333333", lists:flatten(format("~p", [1/3], 50))), | |||||
?assertEqual("0.1234567", lists:flatten(format("~p", [0.1234567], 50))), | |||||
ok. | |||||
float_inside_list_test() -> | |||||
?assertEqual("[97,38.233913133184835,99]", lists:flatten(format("~p", [[$a, 38.233913133184835, $c]], 50))), | |||||
?assertError(badarg, lists:flatten(format("~s", [[$a, 38.233913133184835, $c]], 50))), | |||||
ok. | |||||
quote_strip_test() -> | |||||
?assertEqual("\"hello\"", lists:flatten(format("~p", ["hello"], 50))), | |||||
?assertEqual("hello", lists:flatten(format("~s", ["hello"], 50))), | |||||
?assertEqual("hello", lists:flatten(format("~s", [hello], 50))), | |||||
?assertEqual("hello", lists:flatten(format("~p", [hello], 50))), | |||||
?assertEqual("'hello world'", lists:flatten(format("~p", ['hello world'], 50))), | |||||
?assertEqual("hello world", lists:flatten(format("~s", ['hello world'], 50))), | |||||
ok. | |||||
binary_printing_test() -> | |||||
?assertEqual("<<>>", lists:flatten(format("~p", [<<>>], 50))), | |||||
?assertEqual("", lists:flatten(format("~s", [<<>>], 50))), | |||||
?assertEqual("<<..>>", lists:flatten(format("~p", [<<"hi">>], 0))), | |||||
?assertEqual("<<...>>", lists:flatten(format("~p", [<<"hi">>], 1))), | |||||
?assertEqual("<<\"hello\">>", lists:flatten(format("~p", [<<$h, $e, $l, $l, $o>>], 50))), | |||||
?assertEqual("<<\"hello\">>", lists:flatten(format("~p", [<<"hello">>], 50))), | |||||
?assertEqual("<<104,101,108,108,111>>", lists:flatten(format("~w", [<<"hello">>], 50))), | |||||
?assertEqual("<<1,2,3,4>>", lists:flatten(format("~p", [<<1, 2, 3, 4>>], 50))), | |||||
?assertEqual([1,2,3,4], lists:flatten(format("~s", [<<1, 2, 3, 4>>], 50))), | |||||
?assertEqual("hello", lists:flatten(format("~s", [<<"hello">>], 50))), | |||||
?assertEqual("hello\nworld", lists:flatten(format("~s", [<<"hello\nworld">>], 50))), | |||||
?assertEqual("<<\"hello\\nworld\">>", lists:flatten(format("~p", [<<"hello\nworld">>], 50))), | |||||
?assertEqual("<<\"\\\"hello world\\\"\">>", lists:flatten(format("~p", [<<"\"hello world\"">>], 50))), | |||||
?assertEqual("<<\"hello\\\\world\">>", lists:flatten(format("~p", [<<"hello\\world">>], 50))), | |||||
?assertEqual("<<\"hello\\\\\world\">>", lists:flatten(format("~p", [<<"hello\\\world">>], 50))), | |||||
?assertEqual("<<\"hello\\\\\\\\world\">>", lists:flatten(format("~p", [<<"hello\\\\world">>], 50))), | |||||
?assertEqual("<<\"hello\\bworld\">>", lists:flatten(format("~p", [<<"hello\bworld">>], 50))), | |||||
?assertEqual("<<\"hello\\tworld\">>", lists:flatten(format("~p", [<<"hello\tworld">>], 50))), | |||||
?assertEqual("<<\"hello\\nworld\">>", lists:flatten(format("~p", [<<"hello\nworld">>], 50))), | |||||
?assertEqual("<<\"hello\\rworld\">>", lists:flatten(format("~p", [<<"hello\rworld">>], 50))), | |||||
?assertEqual("<<\"hello\\eworld\">>", lists:flatten(format("~p", [<<"hello\eworld">>], 50))), | |||||
?assertEqual("<<\"hello\\fworld\">>", lists:flatten(format("~p", [<<"hello\fworld">>], 50))), | |||||
?assertEqual("<<\"hello\\vworld\">>", lists:flatten(format("~p", [<<"hello\vworld">>], 50))), | |||||
?assertEqual(" hello", lists:flatten(format("~10s", [<<"hello">>], 50))), | |||||
?assertEqual("[a]", lists:flatten(format("~s", [<<"[a]">>], 50))), | |||||
?assertEqual("[a]", lists:flatten(format("~s", [[<<"[a]">>]], 50))), | |||||
ok. | |||||
bitstring_printing_test() -> | |||||
?assertEqual("<<1,2,3,1:7>>", lists:flatten(format("~p", | |||||
[<<1, 2, 3, 1:7>>], 100))), | |||||
?assertEqual("<<1:7>>", lists:flatten(format("~p", | |||||
[<<1:7>>], 100))), | |||||
?assertEqual("<<1,2,3,...>>", lists:flatten(format("~p", | |||||
[<<1, 2, 3, 1:7>>], 12))), | |||||
?assertEqual("<<1,2,3,...>>", lists:flatten(format("~p", | |||||
[<<1, 2, 3, 1:7>>], 13))), | |||||
?assertEqual("<<1,2,3,1:7>>", lists:flatten(format("~p", | |||||
[<<1, 2, 3, 1:7>>], 14))), | |||||
?assertEqual("<<..>>", lists:flatten(format("~p", [<<1:7>>], 0))), | |||||
?assertEqual("<<...>>", lists:flatten(format("~p", [<<1:7>>], 1))), | |||||
?assertEqual("[<<1>>,<<2>>]", lists:flatten(format("~p", [[<<1>>, <<2>>]], | |||||
100))), | |||||
?assertEqual("{<<1:7>>}", lists:flatten(format("~p", [{<<1:7>>}], 50))), | |||||
ok. | |||||
list_printing_test() -> | |||||
?assertEqual("[]", lists:flatten(format("~p", [[]], 50))), | |||||
?assertEqual("[]", lists:flatten(format("~w", [[]], 50))), | |||||
?assertEqual("", lists:flatten(format("~s", [[]], 50))), | |||||
?assertEqual("...", lists:flatten(format("~s", [[]], -1))), | |||||
?assertEqual("[[]]", lists:flatten(format("~p", [[[]]], 50))), | |||||
?assertEqual("[13,11,10,8,5,4]", lists:flatten(format("~p", [[13,11,10,8,5,4]], 50))), | |||||
?assertEqual("\"\\rabc\"", lists:flatten(format("~p", [[13,$a, $b, $c]], 50))), | |||||
?assertEqual("[1,2,3|4]", lists:flatten(format("~p", [[1, 2, 3|4]], 50))), | |||||
?assertEqual("[...]", lists:flatten(format("~p", [[1, 2, 3,4]], 4))), | |||||
?assertEqual("[1,...]", lists:flatten(format("~p", [[1, 2, 3, 4]], 6))), | |||||
?assertEqual("[1,...]", lists:flatten(format("~p", [[1, 2, 3, 4]], 7))), | |||||
?assertEqual("[1,2,...]", lists:flatten(format("~p", [[1, 2, 3, 4]], 8))), | |||||
?assertEqual("[1|4]", lists:flatten(format("~p", [[1|4]], 50))), | |||||
?assertEqual("[1]", lists:flatten(format("~p", [[1]], 50))), | |||||
?assertError(badarg, lists:flatten(format("~s", [[1|4]], 50))), | |||||
?assertEqual("\"hello...\"", lists:flatten(format("~p", ["hello world"], 10))), | |||||
?assertEqual("hello w...", lists:flatten(format("~s", ["hello world"], 10))), | |||||
?assertEqual("hello world\r\n", lists:flatten(format("~s", ["hello world\r\n"], 50))), | |||||
?assertEqual("\rhello world\r\n", lists:flatten(format("~s", ["\rhello world\r\n"], 50))), | |||||
?assertEqual("\"\\rhello world\\r\\n\"", lists:flatten(format("~p", ["\rhello world\r\n"], 50))), | |||||
?assertEqual("[13,104,101,108,108,111,32,119,111,114,108,100,13,10]", lists:flatten(format("~w", ["\rhello world\r\n"], 60))), | |||||
?assertEqual("...", lists:flatten(format("~s", ["\rhello world\r\n"], 3))), | |||||
?assertEqual("[22835963083295358096932575511191922182123945984,...]", | |||||
lists:flatten(format("~p", [ | |||||
[22835963083295358096932575511191922182123945984, | |||||
22835963083295358096932575511191922182123945984]], 9))), | |||||
?assertEqual("[22835963083295358096932575511191922182123945984,...]", | |||||
lists:flatten(format("~p", [ | |||||
[22835963083295358096932575511191922182123945984, | |||||
22835963083295358096932575511191922182123945984]], 53))), | |||||
%%improper list | |||||
?assertEqual("[1,2,3|4]", lists:flatten(format("~P", [[1|[2|[3|4]]], 5], 50))), | |||||
?assertEqual("[1|1]", lists:flatten(format("~P", [[1|1], 5], 50))), | |||||
?assertEqual("[9|9]", lists:flatten(format("~p", [[9|9]], 50))), | |||||
ok. | |||||
iolist_printing_test() -> | |||||
?assertEqual("iolist: HelloIamaniolist", | |||||
lists:flatten(format("iolist: ~s", [[$H, $e, $l, $l, $o, "I", ["am", [<<"an">>], [$i, $o, $l, $i, $s, $t]]]], 1000))), | |||||
?assertEqual("123...", | |||||
lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 6))), | |||||
?assertEqual("123456...", | |||||
lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 9))), | |||||
?assertEqual("123456789H...", | |||||
lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 13))), | |||||
?assertEqual("123456789HellIamaniolist", | |||||
lists:flatten(format("~s", [[<<"123456789">>, "HellIamaniolist"]], 30))), | |||||
ok. | |||||
tuple_printing_test() -> | |||||
?assertEqual("{}", lists:flatten(format("~p", [{}], 50))), | |||||
?assertEqual("{}", lists:flatten(format("~w", [{}], 50))), | |||||
?assertError(badarg, lists:flatten(format("~s", [{}], 50))), | |||||
?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 1))), | |||||
?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 2))), | |||||
?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 3))), | |||||
?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 4))), | |||||
?assertEqual("{...}", lists:flatten(format("~p", [{foo}], 5))), | |||||
?assertEqual("{foo,...}", lists:flatten(format("~p", [{foo,bar}], 6))), | |||||
?assertEqual("{foo,...}", lists:flatten(format("~p", [{foo,bar}], 7))), | |||||
?assertEqual("{foo,...}", lists:flatten(format("~p", [{foo,bar}], 9))), | |||||
?assertEqual("{foo,bar}", lists:flatten(format("~p", [{foo,bar}], 10))), | |||||
?assertEqual("{22835963083295358096932575511191922182123945984,...}", | |||||
lists:flatten(format("~w", [ | |||||
{22835963083295358096932575511191922182123945984, | |||||
22835963083295358096932575511191922182123945984}], 10))), | |||||
?assertEqual("{22835963083295358096932575511191922182123945984,...}", | |||||
lists:flatten(format("~w", [ | |||||
{22835963083295358096932575511191922182123945984, | |||||
bar}], 10))), | |||||
?assertEqual("{22835963083295358096932575511191922182123945984,...}", | |||||
lists:flatten(format("~w", [ | |||||
{22835963083295358096932575511191922182123945984, | |||||
22835963083295358096932575511191922182123945984}], 53))), | |||||
ok. | |||||
map_printing_test() -> | |||||
?assertEqual("#{}", lists:flatten(format("~p", [maps:new()], 50))), | |||||
?assertEqual("#{}", lists:flatten(format("~p", [maps:new()], 3))), | |||||
?assertEqual("#{}", lists:flatten(format("~w", [maps:new()], 50))), | |||||
?assertError(badarg, lists:flatten(format("~s", [maps:new()], 50))), | |||||
?assertEqual("#{...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 1))), | |||||
?assertEqual("#{...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 6))), | |||||
?assertEqual("#{bar => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 7))), | |||||
?assertEqual("#{bar => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 9))), | |||||
?assertEqual("#{bar => foo}", lists:flatten(format("~p", [maps:from_list([{bar, foo}])], 10))), | |||||
?assertEqual("#{bar => ...,...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 9))), | |||||
?assertEqual("#{bar => foo,...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 10))), | |||||
?assertEqual("#{bar => foo,...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 17))), | |||||
?assertEqual("#{bar => foo,foo => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 18))), | |||||
?assertEqual("#{bar => foo,foo => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 19))), | |||||
?assertEqual("#{bar => foo,foo => ...}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 20))), | |||||
?assertEqual("#{bar => foo,foo => bar}", lists:flatten(format("~p", [maps:from_list([{bar, foo}, {foo, bar}])], 21))), | |||||
?assertEqual("#{22835963083295358096932575511191922182123945984 => ...}", | |||||
lists:flatten(format("~w", [ | |||||
maps:from_list([{22835963083295358096932575511191922182123945984, | |||||
22835963083295358096932575511191922182123945984}])], 10))), | |||||
?assertEqual("#{22835963083295358096932575511191922182123945984 => ...}", | |||||
lists:flatten(format("~w", [ | |||||
maps:from_list([{22835963083295358096932575511191922182123945984, | |||||
bar}])], 10))), | |||||
?assertEqual("#{22835963083295358096932575511191922182123945984 => ...}", | |||||
lists:flatten(format("~w", [ | |||||
maps:from_list([{22835963083295358096932575511191922182123945984, | |||||
bar}])], 53))), | |||||
?assertEqual("#{22835963083295358096932575511191922182123945984 => bar}", | |||||
lists:flatten(format("~w", [ | |||||
maps:from_list([{22835963083295358096932575511191922182123945984, | |||||
bar}])], 54))), | |||||
ok. | |||||
unicode_test() -> | |||||
?assertEqual([231,167,129], lists:flatten(format("~s", [<<231,167,129>>], 50))), | |||||
?assertEqual([31169], lists:flatten(format("~ts", [<<231,167,129>>], 50))), | |||||
ok. | |||||
depth_limit_test() -> | |||||
?assertEqual("{...}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 1], 50))), | |||||
?assertEqual("{a,...}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 2], 50))), | |||||
?assertEqual("{a,[...]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 3], 50))), | |||||
?assertEqual("{a,[b|...]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 4], 50))), | |||||
?assertEqual("{a,[b,[...]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 5], 50))), | |||||
?assertEqual("{a,[b,[c|...]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 6], 50))), | |||||
?assertEqual("{a,[b,[c,[...]]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 7], 50))), | |||||
?assertEqual("{a,[b,[c,[d]]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 8], 50))), | |||||
?assertEqual("{a,[b,[c,[d]]]}", lists:flatten(format("~P", [{a, [b, [c, [d]]]}, 9], 50))), | |||||
?assertEqual("{a,{...}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 3], 50))), | |||||
?assertEqual("{a,{b,...}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 4], 50))), | |||||
?assertEqual("{a,{b,{...}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 5], 50))), | |||||
?assertEqual("{a,{b,{c,...}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 6], 50))), | |||||
?assertEqual("{a,{b,{c,{...}}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 7], 50))), | |||||
?assertEqual("{a,{b,{c,{d}}}}", lists:flatten(format("~P", [{a, {b, {c, {d}}}}, 8], 50))), | |||||
?assertEqual("#{a => #{...}}", | |||||
lists:flatten(format("~P", | |||||
[maps:from_list([{a, maps:from_list([{b, maps:from_list([{c, d}])}])}]), 2], 50))), | |||||
?assertEqual("#{a => #{b => #{...}}}", | |||||
lists:flatten(format("~P", | |||||
[maps:from_list([{a, maps:from_list([{b, maps:from_list([{c, d}])}])}]), 3], 50))), | |||||
?assertEqual("#{a => #{b => #{c => d}}}", | |||||
lists:flatten(format("~P", | |||||
[maps:from_list([{a, maps:from_list([{b, maps:from_list([{c, d}])}])}]), 4], 50))), | |||||
?assertEqual("#{}", lists:flatten(format("~P", [maps:new(), 1], 50))), | |||||
?assertEqual("#{...}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 1], 50))), | |||||
?assertEqual("#{1 => 1,...}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 2], 50))), | |||||
?assertEqual("#{1 => 1,2 => 2,...}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 3], 50))), | |||||
?assertEqual("#{1 => 1,2 => 2,3 => 3}", lists:flatten(format("~P", [maps:from_list([{1,1}, {2,2}, {3,3}]), 4], 50))), | |||||
?assertEqual("{\"a\",[...]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 3], 50))), | |||||
?assertEqual("{\"a\",[\"b\",[[...]|...]]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 6], 50))), | |||||
?assertEqual("{\"a\",[\"b\",[\"c\",[\"d\"]]]}", lists:flatten(format("~P", [{"a", ["b", ["c", ["d"]]]}, 9], 50))), | |||||
?assertEqual("[...]", lists:flatten(format("~P", [[1, 2, 3], 1], 50))), | |||||
?assertEqual("[1|...]", lists:flatten(format("~P", [[1, 2, 3], 2], 50))), | |||||
?assertEqual("[1,2|...]", lists:flatten(format("~P", [[1, 2, 3], 3], 50))), | |||||
?assertEqual("[1,2,3]", lists:flatten(format("~P", [[1, 2, 3], 4], 50))), | |||||
?assertEqual("{1,...}", lists:flatten(format("~P", [{1, 2, 3}, 2], 50))), | |||||
?assertEqual("{1,2,...}", lists:flatten(format("~P", [{1, 2, 3}, 3], 50))), | |||||
?assertEqual("{1,2,3}", lists:flatten(format("~P", [{1, 2, 3}, 4], 50))), | |||||
?assertEqual("{1,...}", lists:flatten(format("~P", [{1, 2, 3}, 2], 50))), | |||||
?assertEqual("[1,2|...]", lists:flatten(format("~P", [[1, 2, <<3>>], 3], 50))), | |||||
?assertEqual("[1,2,<<...>>]", lists:flatten(format("~P", [[1, 2, <<3>>], 4], 50))), | |||||
?assertEqual("[1,2,<<3>>]", lists:flatten(format("~P", [[1, 2, <<3>>], 5], 50))), | |||||
?assertEqual("<<...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 1], 50))), | |||||
?assertEqual("<<0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 2], 50))), | |||||
?assertEqual("<<0,0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 3], 50))), | |||||
?assertEqual("<<0,0,0,...>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 4], 50))), | |||||
?assertEqual("<<0,0,0,0>>", lists:flatten(format("~P", [<<0, 0, 0, 0>>, 5], 50))), | |||||
%% this is a seriously weird edge case | |||||
?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 2], 50))), | |||||
?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 3], 50))), | |||||
?assertEqual("<<\" \"...>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 4], 50))), | |||||
?assertEqual("<<32,32,32,0>>", lists:flatten(format("~P", [<<32, 32, 32, 0>>, 5], 50))), | |||||
?assertEqual("<<32,32,32,0>>", lists:flatten(format("~p", [<<32, 32, 32, 0>>], 50))), | |||||
%% depth limiting for some reason works in 4 byte chunks on printable binaries? | |||||
?assertEqual("<<\"hell\"...>>", lists:flatten(format("~P", [<<"hello world">>, 2], 50))), | |||||
?assertEqual("<<\"abcd\"...>>", lists:flatten(format("~P", [<<$a, $b, $c, $d, $e, 0>>, 2], 50))), | |||||
%% I don't even know... | |||||
?assertEqual("<<>>", lists:flatten(format("~P", [<<>>, 1], 50))), | |||||
?assertEqual("<<>>", lists:flatten(format("~W", [<<>>, 1], 50))), | |||||
?assertEqual("{abc,<<\"abc\\\"\">>}", lists:flatten(format("~P", [{abc,<<"abc\"">>}, 4], 50))), | |||||
ok. | |||||
print_terms_without_format_string_test() -> | |||||
?assertError(badarg, format({hello, world}, [], 50)), | |||||
?assertError(badarg, format([{google, bomb}], [], 50)), | |||||
?assertEqual([$h,$e,$l,$l,$o, 3594], format([$h,$e,$l,$l,$o, 3594], [], 50)), | |||||
?assertError(badarg, format([$h,$e,$l,$l,$o, 65535], [], 50)), | |||||
?assertEqual("helloworld", lists:flatten(format([$h,$e,$l,$l,$o, "world"], [], 50))), | |||||
?assertEqual("hello", lists:flatten(format(<<"hello">>, [], 50))), | |||||
?assertEqual("hello", lists:flatten(format('hello', [], 50))), | |||||
?assertError(badarg, format(<<1, 2, 3, 1:7>>, [], 100)), | |||||
?assertError(badarg, format(65535, [], 50)), | |||||
ok. | |||||
improper_io_list_test() -> | |||||
?assertEqual(">hello", lists:flatten(format('~s', [[$>|<<"hello">>]], 50))), | |||||
?assertEqual(">hello", lists:flatten(format('~ts', [[$>|<<"hello">>]], 50))), | |||||
?assertEqual("helloworld", lists:flatten(format('~ts', [[<<"hello">>|<<"world">>]], 50))), | |||||
ok. | |||||
-endif. |
@ -0,0 +1,967 @@ | |||||
%% ------------------------------------------------------------------- | |||||
%% | |||||
%% 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. | |||||
%% | |||||
%% ------------------------------------------------------------------- | |||||
-module(lager_util). | |||||
-export([ | |||||
levels/0, level_to_num/1, level_to_chr/1, | |||||
num_to_level/1, config_to_mask/1, config_to_levels/1, mask_to_levels/1, | |||||
format_time/0, format_time/1, | |||||
localtime_ms/0, localtime_ms/1, maybe_utc/1, parse_rotation_date_spec/1, | |||||
calculate_next_rotation/1, validate_trace/1, check_traces/4, is_loggable/3, | |||||
trace_filter/1, trace_filter/2, expand_path/1, find_file/2, check_hwm/1, check_hwm/2, | |||||
make_internal_sink_name/1, otp_version/0, maybe_flush/2, | |||||
has_file_changed/3 | |||||
]). | |||||
-ifdef(TEST). | |||||
-export([create_test_dir/0, get_test_dir/0, delete_test_dir/0, | |||||
set_dir_permissions/2, | |||||
safe_application_load/1, | |||||
safe_write_file/2]). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
-include("lager.hrl"). | |||||
-include_lib("kernel/include/file.hrl"). | |||||
levels() -> | |||||
[debug, info, notice, warning, error, critical, alert, emergency, none]. | |||||
level_to_num(debug) -> ?DEBUG; | |||||
level_to_num(info) -> ?INFO; | |||||
level_to_num(notice) -> ?NOTICE; | |||||
level_to_num(warning) -> ?WARNING; | |||||
level_to_num(error) -> ?ERROR; | |||||
level_to_num(critical) -> ?CRITICAL; | |||||
level_to_num(alert) -> ?ALERT; | |||||
level_to_num(emergency) -> ?EMERGENCY; | |||||
level_to_num(none) -> ?LOG_NONE. | |||||
level_to_chr(debug) -> $D; | |||||
level_to_chr(info) -> $I; | |||||
level_to_chr(notice) -> $N; | |||||
level_to_chr(warning) -> $W; | |||||
level_to_chr(error) -> $E; | |||||
level_to_chr(critical) -> $C; | |||||
level_to_chr(alert) -> $A; | |||||
level_to_chr(emergency) -> $M; | |||||
level_to_chr(none) -> $ . | |||||
num_to_level(?DEBUG) -> debug; | |||||
num_to_level(?INFO) -> info; | |||||
num_to_level(?NOTICE) -> notice; | |||||
num_to_level(?WARNING) -> warning; | |||||
num_to_level(?ERROR) -> error; | |||||
num_to_level(?CRITICAL) -> critical; | |||||
num_to_level(?ALERT) -> alert; | |||||
num_to_level(?EMERGENCY) -> emergency; | |||||
num_to_level(?LOG_NONE) -> none. | |||||
-spec config_to_mask(atom()|string()) -> {'mask', integer()}. | |||||
config_to_mask(Conf) -> | |||||
Levels = config_to_levels(Conf), | |||||
{mask, lists:foldl(fun(Level, Acc) -> | |||||
level_to_num(Level) bor Acc | |||||
end, 0, Levels)}. | |||||
-spec mask_to_levels(non_neg_integer()) -> [lager:log_level()]. | |||||
mask_to_levels(Mask) -> | |||||
mask_to_levels(Mask, levels(), []). | |||||
mask_to_levels(_Mask, [], Acc) -> | |||||
lists:reverse(Acc); | |||||
mask_to_levels(Mask, [Level|Levels], Acc) -> | |||||
NewAcc = case (level_to_num(Level) band Mask) /= 0 of | |||||
true -> | |||||
[Level|Acc]; | |||||
false -> | |||||
Acc | |||||
end, | |||||
mask_to_levels(Mask, Levels, NewAcc). | |||||
-spec config_to_levels(atom()|string()) -> [lager:log_level()]. | |||||
config_to_levels(Conf) when is_atom(Conf) -> | |||||
config_to_levels(atom_to_list(Conf)); | |||||
config_to_levels([$! | Rest]) -> | |||||
levels() -- config_to_levels(Rest); | |||||
config_to_levels([$=, $< | Rest]) -> | |||||
[_|Levels] = config_to_levels_int(Rest), | |||||
lists:filter(fun(E) -> not lists:member(E, Levels) end, levels()); | |||||
config_to_levels([$<, $= | Rest]) -> | |||||
[_|Levels] = config_to_levels_int(Rest), | |||||
lists:filter(fun(E) -> not lists:member(E, Levels) end, levels()); | |||||
config_to_levels([$>, $= | Rest]) -> | |||||
config_to_levels_int(Rest); | |||||
config_to_levels([$=, $> | Rest]) -> | |||||
config_to_levels_int(Rest); | |||||
config_to_levels([$= | Rest]) -> | |||||
[level_to_atom(Rest)]; | |||||
config_to_levels([$< | Rest]) -> | |||||
Levels = config_to_levels_int(Rest), | |||||
lists:filter(fun(E) -> not lists:member(E, Levels) end, levels()); | |||||
config_to_levels([$> | Rest]) -> | |||||
[_|Levels] = config_to_levels_int(Rest), | |||||
lists:filter(fun(E) -> lists:member(E, Levels) end, levels()); | |||||
config_to_levels(Conf) -> | |||||
config_to_levels_int(Conf). | |||||
%% internal function to break the recursion loop | |||||
config_to_levels_int(Conf) -> | |||||
Level = level_to_atom(Conf), | |||||
lists:dropwhile(fun(E) -> E /= Level end, levels()). | |||||
level_to_atom(String) -> | |||||
Levels = levels(), | |||||
try list_to_existing_atom(String) of | |||||
Atom -> | |||||
case lists:member(Atom, Levels) of | |||||
true -> | |||||
Atom; | |||||
false -> | |||||
erlang:error(badarg) | |||||
end | |||||
catch | |||||
_:_ -> | |||||
erlang:error(badarg) | |||||
end. | |||||
%% returns localtime with milliseconds included | |||||
localtime_ms() -> | |||||
Now = os:timestamp(), | |||||
localtime_ms(Now). | |||||
localtime_ms(Now) -> | |||||
{_, _, Micro} = Now, | |||||
{Date, {Hours, Minutes, Seconds}} = calendar:now_to_local_time(Now), | |||||
{Date, {Hours, Minutes, Seconds, Micro div 1000 rem 1000}}. | |||||
maybe_utc({Date, {H, M, S, Ms}}) -> | |||||
case lager_stdlib:maybe_utc({Date, {H, M, S}}) of | |||||
{utc, {Date1, {H1, M1, S1}}} -> | |||||
{utc, {Date1, {H1, M1, S1, Ms}}}; | |||||
{Date1, {H1, M1, S1}} -> | |||||
{Date1, {H1, M1, S1, Ms}} | |||||
end. | |||||
format_time() -> | |||||
format_time(maybe_utc(localtime_ms())). | |||||
format_time({utc, {{Y, M, D}, {H, Mi, S, Ms}}}) -> | |||||
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], | |||||
[i2l(H), $:, i2l(Mi), $:, i2l(S), $., i3l(Ms), $ , $U, $T, $C]}; | |||||
format_time({{Y, M, D}, {H, Mi, S, Ms}}) -> | |||||
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], | |||||
[i2l(H), $:, i2l(Mi), $:, i2l(S), $., i3l(Ms)]}; | |||||
format_time({utc, {{Y, M, D}, {H, Mi, S}}}) -> | |||||
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], | |||||
[i2l(H), $:, i2l(Mi), $:, i2l(S), $ , $U, $T, $C]}; | |||||
format_time({{Y, M, D}, {H, Mi, S}}) -> | |||||
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)], | |||||
[i2l(H), $:, i2l(Mi), $:, i2l(S)]}. | |||||
parse_rotation_hour_spec([], Res) -> | |||||
{ok, Res}; | |||||
parse_rotation_hour_spec([$H, M1, M2], Res) -> | |||||
case list_to_integer([M1, M2]) of | |||||
X when X >= 0, X =< 59 -> | |||||
{ok, Res ++ [{minute, X}]}; | |||||
_ -> | |||||
{error, invalid_date_spec} | |||||
end; | |||||
parse_rotation_hour_spec([$H, M], Res) when M >= $0, M =< $9 -> | |||||
{ok, Res ++ [{minute, M - 48}]}; | |||||
parse_rotation_hour_spec(_,_) -> | |||||
{error, invalid_date_spec}. | |||||
%% Default to 00:00:00 rotation | |||||
parse_rotation_day_spec([], Res) -> | |||||
{ok, Res ++ [{hour ,0}]}; | |||||
parse_rotation_day_spec([$D, D1, D2|T], Res) -> | |||||
case list_to_integer([D1, D2]) of | |||||
X when X >= 0, X =< 23 -> | |||||
parse_rotation_hour_spec(T, Res ++ [{hour, X}]); | |||||
_ -> | |||||
{error, invalid_date_spec} | |||||
end; | |||||
parse_rotation_day_spec([$D, D|T], Res) when D >= $0, D =< $9 -> | |||||
parse_rotation_hour_spec(T, Res ++ [{hour, D - 48 }]); | |||||
parse_rotation_day_spec(X, Res) -> | |||||
parse_rotation_hour_spec(X, Res). | |||||
parse_rotation_date_spec([$$, $W, W|T]) when W >= $0, W =< $6 -> | |||||
Week = W - 48, | |||||
parse_rotation_day_spec(T, [{day, Week}]); | |||||
parse_rotation_date_spec([$$, $M, L|T]) when L == $L; L == $l -> | |||||
%% last day in month. | |||||
parse_rotation_day_spec(T, [{date, last}]); | |||||
parse_rotation_date_spec([$$, $M, M1, M2|[$D|_]=T]) -> | |||||
case list_to_integer([M1, M2]) of | |||||
X when X >= 1, X =< 31 -> | |||||
parse_rotation_day_spec(T, [{date, X}]); | |||||
_ -> | |||||
{error, invalid_date_spec} | |||||
end; | |||||
parse_rotation_date_spec([$$, $M, M|[$D|_]=T]) -> | |||||
parse_rotation_day_spec(T, [{date, M - 48}]); | |||||
parse_rotation_date_spec([$$, $M, M1, M2]) -> | |||||
case list_to_integer([M1, M2]) of | |||||
X when X >= 1, X =< 31 -> | |||||
{ok, [{date, X}, {hour, 0}]}; | |||||
_ -> | |||||
{error, invalid_date_spec} | |||||
end; | |||||
parse_rotation_date_spec([$$, $M, M]) -> | |||||
{ok, [{date, M - 48}, {hour, 0}]}; | |||||
parse_rotation_date_spec([$$|X]) when X /= [] -> | |||||
parse_rotation_day_spec(X, []); | |||||
parse_rotation_date_spec(_) -> | |||||
{error, invalid_date_spec}. | |||||
calculate_next_rotation(Spec) -> | |||||
Now = calendar:local_time(), | |||||
Later = calculate_next_rotation(Spec, Now), | |||||
calendar:datetime_to_gregorian_seconds(Later) - | |||||
calendar:datetime_to_gregorian_seconds(Now). | |||||
calculate_next_rotation([], Now) -> | |||||
Now; | |||||
calculate_next_rotation([{minute, X}|T], {{_, _, _}, {Hour, Minute, _}} = Now) when Minute < X -> | |||||
%% rotation is this hour | |||||
NewNow = setelement(2, Now, {Hour, X, 0}), | |||||
calculate_next_rotation(T, NewNow); | |||||
calculate_next_rotation([{minute, X}|T], Now) -> | |||||
%% rotation is next hour | |||||
Seconds = calendar:datetime_to_gregorian_seconds(Now) + 3600, | |||||
DateTime = calendar:gregorian_seconds_to_datetime(Seconds), | |||||
{_, {NewHour, _, _}} = DateTime, | |||||
NewNow = setelement(2, DateTime, {NewHour, X, 0}), | |||||
calculate_next_rotation(T, NewNow); | |||||
calculate_next_rotation([{hour, X}|T], {{_, _, _}, {Hour, _, _}} = Now) when Hour < X -> | |||||
%% rotation is today, sometime | |||||
NewNow = setelement(2, Now, {X, 0, 0}), | |||||
calculate_next_rotation(T, NewNow); | |||||
calculate_next_rotation([{hour, X}|T], {{_, _, _}, _} = Now) -> | |||||
%% rotation is not today | |||||
Seconds = calendar:datetime_to_gregorian_seconds(Now) + 86400, | |||||
DateTime = calendar:gregorian_seconds_to_datetime(Seconds), | |||||
NewNow = setelement(2, DateTime, {X, 0, 0}), | |||||
calculate_next_rotation(T, NewNow); | |||||
calculate_next_rotation([{day, Day}|T], {Date, _Time} = Now) -> | |||||
DoW = calendar:day_of_the_week(Date), | |||||
AdjustedDay = case Day of | |||||
0 -> 7; | |||||
X -> X | |||||
end, | |||||
case AdjustedDay of | |||||
DoW -> %% rotation is today | |||||
case calculate_next_rotation(T, Now) of | |||||
{Date, _} = NewNow -> NewNow; | |||||
{NewDate, _} -> | |||||
%% rotation *isn't* today! rerun the calculation | |||||
NewNow = {NewDate, {0, 0, 0}}, | |||||
calculate_next_rotation([{day, Day}|T], NewNow) | |||||
end; | |||||
Y when Y > DoW -> %% rotation is later this week | |||||
PlusDays = Y - DoW, | |||||
Seconds = calendar:datetime_to_gregorian_seconds(Now) + (86400 * PlusDays), | |||||
{NewDate, _} = calendar:gregorian_seconds_to_datetime(Seconds), | |||||
NewNow = {NewDate, {0, 0, 0}}, | |||||
calculate_next_rotation(T, NewNow); | |||||
Y when Y < DoW -> %% rotation is next week | |||||
PlusDays = ((7 - DoW) + Y), | |||||
Seconds = calendar:datetime_to_gregorian_seconds(Now) + (86400 * PlusDays), | |||||
{NewDate, _} = calendar:gregorian_seconds_to_datetime(Seconds), | |||||
NewNow = {NewDate, {0, 0, 0}}, | |||||
calculate_next_rotation(T, NewNow) | |||||
end; | |||||
calculate_next_rotation([{date, last}|T], {{Year, Month, Day}, _} = Now) -> | |||||
Last = calendar:last_day_of_the_month(Year, Month), | |||||
case Last == Day of | |||||
true -> %% doing rotation today | |||||
case calculate_next_rotation(T, Now) of | |||||
{{Year, Month, Day}, _} = NewNow -> NewNow; | |||||
{NewDate, _} -> | |||||
%% rotation *isn't* today! rerun the calculation | |||||
NewNow = {NewDate, {0, 0, 0}}, | |||||
calculate_next_rotation([{date, last}|T], NewNow) | |||||
end; | |||||
false -> | |||||
NewNow = setelement(1, Now, {Year, Month, Last}), | |||||
calculate_next_rotation(T, NewNow) | |||||
end; | |||||
calculate_next_rotation([{date, Date}|T], {{Year, Month, Date}, _} = Now) -> | |||||
%% rotation is today | |||||
case calculate_next_rotation(T, Now) of | |||||
{{Year, Month, Date}, _} = NewNow -> NewNow; | |||||
{NewDate, _} -> | |||||
%% rotation *isn't* today! rerun the calculation | |||||
NewNow = setelement(1, Now, NewDate), | |||||
calculate_next_rotation([{date, Date}|T], NewNow) | |||||
end; | |||||
calculate_next_rotation([{date, Date}|T], {{Year, Month, Day}, _} = Now) -> | |||||
PlusDays = case Date of | |||||
X when X < Day -> %% rotation is next month | |||||
Last = calendar:last_day_of_the_month(Year, Month), | |||||
(Last - Day); | |||||
X when X > Day -> %% rotation is later this month | |||||
X - Day | |||||
end, | |||||
Seconds = calendar:datetime_to_gregorian_seconds(Now) + (86400 * PlusDays), | |||||
NewNow = calendar:gregorian_seconds_to_datetime(Seconds), | |||||
calculate_next_rotation(T, NewNow). | |||||
-spec trace_filter(Query :: 'none' | [tuple()]) -> {ok, any()}. | |||||
trace_filter(Query) -> | |||||
trace_filter(?DEFAULT_TRACER, Query). | |||||
%% TODO: Support multiple trace modules | |||||
%-spec trace_filter(Module :: atom(), Query :: 'none' | [tuple()]) -> {ok, any()}. | |||||
trace_filter(Module, Query) when Query == none; Query == [] -> | |||||
{ok, _} = glc:compile(Module, glc:null(false)); | |||||
trace_filter(Module, Query) when is_list(Query) -> | |||||
{ok, _} = glc:compile(Module, glc_lib:reduce(trace_any(Query))). | |||||
validate_trace({Filter, Level, {Destination, ID}}) when is_tuple(Filter); is_list(Filter), is_atom(Level), is_atom(Destination) -> | |||||
case validate_trace({Filter, Level, Destination}) of | |||||
{ok, {F, L, D}} -> | |||||
{ok, {F, L, {D, ID}}}; | |||||
Error -> | |||||
Error | |||||
end; | |||||
validate_trace({Filter, Level, Destination}) when is_tuple(Filter); is_list(Filter), is_atom(Level), is_atom(Destination) -> | |||||
ValidFilter = validate_trace_filter(Filter), | |||||
try config_to_mask(Level) of | |||||
_ when not ValidFilter -> | |||||
{error, invalid_trace}; | |||||
L when is_list(Filter) -> | |||||
{ok, {trace_all(Filter), L, Destination}}; | |||||
L -> | |||||
{ok, {Filter, L, Destination}} | |||||
catch | |||||
_:_ -> | |||||
{error, invalid_level} | |||||
end; | |||||
validate_trace(_) -> | |||||
{error, invalid_trace}. | |||||
validate_trace_filter(Filter) when is_tuple(Filter), is_atom(element(1, Filter)) =:= false -> | |||||
false; | |||||
validate_trace_filter(Filter) when is_list(Filter) -> | |||||
lists:all(fun validate_trace_filter/1, Filter); | |||||
validate_trace_filter({Key, '*'}) when is_atom(Key) -> true; | |||||
validate_trace_filter({any, L}) when is_list(L) -> lists:all(fun validate_trace_filter/1, L); | |||||
validate_trace_filter({all, L}) when is_list(L) -> lists:all(fun validate_trace_filter/1, L); | |||||
validate_trace_filter({null, Bool}) when is_boolean(Bool) -> true; | |||||
validate_trace_filter({Key, _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter({Key, '=', _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter({Key, '!=', _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter({Key, '<', _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter({Key, '=<', _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter({Key, '>', _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter({Key, '>=', _Value}) when is_atom(Key) -> true; | |||||
validate_trace_filter(_) -> false. | |||||
trace_all(Query) -> | |||||
glc:all(trace_acc(Query)). | |||||
trace_any(Query) -> | |||||
glc:any(Query). | |||||
trace_acc(Query) -> | |||||
trace_acc(Query, []). | |||||
trace_acc([], Acc) -> | |||||
lists:reverse(Acc); | |||||
trace_acc([{any, L}|T], Acc) -> | |||||
trace_acc(T, [glc:any(L)|Acc]); | |||||
trace_acc([{all, L}|T], Acc) -> | |||||
trace_acc(T, [glc:all(L)|Acc]); | |||||
trace_acc([{null, Bool}|T], Acc) -> | |||||
trace_acc(T, [glc:null(Bool)|Acc]); | |||||
trace_acc([{Key, '*'}|T], Acc) -> | |||||
trace_acc(T, [glc:wc(Key)|Acc]); | |||||
trace_acc([{Key, '!'}|T], Acc) -> | |||||
trace_acc(T, [glc:nf(Key)|Acc]); | |||||
trace_acc([{Key, Val}|T], Acc) -> | |||||
trace_acc(T, [glc:eq(Key, Val)|Acc]); | |||||
trace_acc([{Key, '=', Val}|T], Acc) -> | |||||
trace_acc(T, [glc:eq(Key, Val)|Acc]); | |||||
trace_acc([{Key, '!=', Val}|T], Acc) -> | |||||
trace_acc(T, [glc:neq(Key, Val)|Acc]); | |||||
trace_acc([{Key, '>', Val}|T], Acc) -> | |||||
trace_acc(T, [glc:gt(Key, Val)|Acc]); | |||||
trace_acc([{Key, '>=', Val}|T], Acc) -> | |||||
trace_acc(T, [glc:gte(Key, Val)|Acc]); | |||||
trace_acc([{Key, '=<', Val}|T], Acc) -> | |||||
trace_acc(T, [glc:lte(Key, Val)|Acc]); | |||||
trace_acc([{Key, '<', Val}|T], Acc) -> | |||||
trace_acc(T, [glc:lt(Key, Val)|Acc]). | |||||
check_traces(_, _, [], Acc) -> | |||||
lists:flatten(Acc); | |||||
check_traces(Attrs, Level, [{_, {mask, FilterLevel}, _}|Flows], Acc) when (Level band FilterLevel) == 0 -> | |||||
check_traces(Attrs, Level, Flows, Acc); | |||||
check_traces(Attrs, Level, [{Filter, _, _}|Flows], Acc) when length(Attrs) < length(Filter) -> | |||||
check_traces(Attrs, Level, Flows, Acc); | |||||
check_traces(Attrs, Level, [Flow|Flows], Acc) -> | |||||
check_traces(Attrs, Level, Flows, [check_trace(Attrs, Flow)|Acc]). | |||||
check_trace(Attrs, {Filter, _Level, Dest}) when is_list(Filter) -> | |||||
check_trace(Attrs, {trace_all(Filter), _Level, Dest}); | |||||
check_trace(Attrs, {Filter, _Level, Dest}) when is_tuple(Filter) -> | |||||
Made = gre:make(Attrs, [list]), | |||||
glc:handle(?DEFAULT_TRACER, Made), | |||||
Match = glc_lib:matches(Filter, Made), | |||||
case Match of | |||||
true -> | |||||
Dest; | |||||
false -> | |||||
[] | |||||
end. | |||||
-spec is_loggable(lager_msg:lager_msg(), non_neg_integer()|{'mask', non_neg_integer()}, term()) -> boolean(). | |||||
is_loggable(Msg, {mask, Mask}, MyName) -> | |||||
%% using syslog style comparison flags | |||||
%S = lager_msg:severity_as_int(Msg), | |||||
%?debugFmt("comparing masks ~.2B and ~.2B -> ~p~n", [S, Mask, S band Mask]), | |||||
(lager_msg:severity_as_int(Msg) band Mask) /= 0 orelse | |||||
lists:member(MyName, lager_msg:destinations(Msg)); | |||||
is_loggable(Msg, SeverityThreshold, MyName) when is_atom(SeverityThreshold) -> | |||||
is_loggable(Msg, level_to_num(SeverityThreshold), MyName); | |||||
is_loggable(Msg, SeverityThreshold, MyName) when is_integer(SeverityThreshold) -> | |||||
lager_msg:severity_as_int(Msg) =< SeverityThreshold orelse | |||||
lists:member(MyName, lager_msg:destinations(Msg)). | |||||
i2l(I) when I < 10 -> [$0, $0+I]; | |||||
i2l(I) -> integer_to_list(I). | |||||
i3l(I) when I < 100 -> [$0 | i2l(I)]; | |||||
i3l(I) -> integer_to_list(I). | |||||
%% When log_root option is provided, get the real path to a file | |||||
expand_path(RelPath) -> | |||||
case application:get_env(lager, log_root) of | |||||
{ok, LogRoot} when is_list(LogRoot) -> % Join relative path | |||||
%% check if the given RelPath contains LogRoot, if so, do not add | |||||
%% it again; see gh #304 | |||||
case string:str(filename:dirname(RelPath), LogRoot) of | |||||
X when X > 0 -> | |||||
RelPath; | |||||
_Zero -> | |||||
filename:join(LogRoot, RelPath) | |||||
end; | |||||
undefined -> % No log_root given, keep relative path | |||||
RelPath | |||||
end. | |||||
%% Find a file among the already installed handlers. | |||||
%% | |||||
%% The file is already expanded (i.e. lager_util:expand_path already added the | |||||
%% "log_root"), but the file paths inside Handlers are not. | |||||
find_file(_File1, _Handlers = []) -> | |||||
false; | |||||
find_file(File1, [{{lager_file_backend, File2}, _Handler, _Sink} = HandlerInfo | Handlers]) -> | |||||
File1Abs = filename:absname(File1), | |||||
File2Abs = filename:absname(lager_util:expand_path(File2)), | |||||
case File1Abs =:= File2Abs of | |||||
true -> | |||||
% The file inside HandlerInfo is the same as the file we are looking | |||||
% for, so we are done. | |||||
HandlerInfo; | |||||
false -> | |||||
find_file(File1, Handlers) | |||||
end; | |||||
find_file(File1, [_HandlerInfo | Handlers]) -> | |||||
find_file(File1, Handlers). | |||||
%% conditionally check the HWM if the event would not have been filtered | |||||
check_hwm(Shaper = #lager_shaper{filter = Filter}, Event) -> | |||||
case Filter(Event) of | |||||
true -> | |||||
{true, 0, Shaper}; | |||||
false -> | |||||
check_hwm(Shaper) | |||||
end. | |||||
%% Log rate limit, i.e. high water mark for incoming messages | |||||
check_hwm(Shaper = #lager_shaper{hwm = undefined}) -> | |||||
{true, 0, Shaper}; | |||||
check_hwm(Shaper = #lager_shaper{mps = Mps, hwm = Hwm, lasttime = Last}) when Mps < Hwm -> | |||||
{M, S, _} = Now = os:timestamp(), | |||||
case Last of | |||||
{M, S, _} -> | |||||
{true, 0, Shaper#lager_shaper{mps=Mps+1}}; | |||||
_ -> | |||||
%different second - reset mps | |||||
{true, 0, Shaper#lager_shaper{mps=1, lasttime = Now}} | |||||
end; | |||||
check_hwm(Shaper = #lager_shaper{lasttime = Last, dropped = Drop}) -> | |||||
%% are we still in the same second? | |||||
{M, S, _} = Now = os:timestamp(), | |||||
case Last of | |||||
{M, S, N} -> | |||||
%% still in same second, but have exceeded the high water mark | |||||
NewDrops = case should_flush(Shaper) of | |||||
true -> | |||||
discard_messages(Now, Shaper#lager_shaper.filter, 0); | |||||
false -> | |||||
0 | |||||
end, | |||||
Timer = case erlang:read_timer(Shaper#lager_shaper.timer) of | |||||
false -> | |||||
erlang:send_after(trunc((1000000 - N)/1000), self(), {shaper_expired, Shaper#lager_shaper.id}); | |||||
_ -> | |||||
Shaper#lager_shaper.timer | |||||
end, | |||||
{false, 0, Shaper#lager_shaper{dropped=Drop+NewDrops, timer=Timer}}; | |||||
_ -> | |||||
_ = erlang:cancel_timer(Shaper#lager_shaper.timer), | |||||
%% different second, reset all counters and allow it | |||||
{true, Drop, Shaper#lager_shaper{dropped = 0, mps=0, lasttime = Now}} | |||||
end. | |||||
should_flush(#lager_shaper{flush_queue = true, flush_threshold = 0}) -> | |||||
true; | |||||
should_flush(#lager_shaper{flush_queue = true, flush_threshold = T}) -> | |||||
{_, L} = process_info(self(), message_queue_len), | |||||
L > T; | |||||
should_flush(_) -> | |||||
false. | |||||
discard_messages(Second, Filter, Count) -> | |||||
{M, S, _} = os:timestamp(), | |||||
case Second of | |||||
{M, S, _} -> | |||||
receive | |||||
%% we only discard gen_event notifications, because | |||||
%% otherwise we might discard gen_event internal | |||||
%% messages, such as trapped EXITs | |||||
{notify, Event} -> | |||||
NewCount = case Filter(Event) of | |||||
false -> Count+1; | |||||
true -> Count | |||||
end, | |||||
discard_messages(Second, Filter, NewCount) | |||||
after 0 -> | |||||
Count | |||||
end; | |||||
_ -> | |||||
Count | |||||
end. | |||||
%% @private Build an atom for the gen_event process based on a sink name. | |||||
%% For historical reasons, the default gen_event process for lager itself is named | |||||
%% `lager_event'. For all other sinks, it is SinkName++`_lager_event' | |||||
make_internal_sink_name(lager) -> | |||||
?DEFAULT_SINK; | |||||
make_internal_sink_name(Sink) -> | |||||
list_to_atom(atom_to_list(Sink) ++ "_lager_event"). | |||||
-spec otp_version() -> pos_integer(). | |||||
%% @doc Return the major version of the current Erlang/OTP runtime as an integer. | |||||
otp_version() -> | |||||
{Vsn, _} = string:to_integer( | |||||
case erlang:system_info(otp_release) of | |||||
[$R | Rel] -> | |||||
Rel; | |||||
Rel -> | |||||
Rel | |||||
end), | |||||
Vsn. | |||||
maybe_flush(undefined, #lager_shaper{} = S) -> | |||||
S; | |||||
maybe_flush(Flag, #lager_shaper{} = S) when is_boolean(Flag) -> | |||||
S#lager_shaper{flush_queue = Flag}. | |||||
-spec has_file_changed(Name :: file:name_all(), | |||||
Inode0 :: pos_integer(), | |||||
Ctime0 :: file:date_time()) -> {boolean(), file:file_info() | undefined}. | |||||
has_file_changed(Name, Inode0, Ctime0) -> | |||||
{OsType, _} = os:type(), | |||||
F = file:read_file_info(Name, [raw]), | |||||
case {OsType, F} of | |||||
{win32, {ok, #file_info{ctime=Ctime1}=FInfo}} -> | |||||
% Note: on win32, Inode is always zero | |||||
% So check the file's ctime to see if it | |||||
% needs to be re-opened | |||||
Changed = Ctime0 =/= Ctime1, | |||||
{Changed, FInfo}; | |||||
{_, {ok, #file_info{inode=Inode1}=FInfo}} -> | |||||
Changed = Inode0 =/= Inode1, | |||||
{Changed, FInfo}; | |||||
{_, _} -> | |||||
{true, undefined} | |||||
end. | |||||
-ifdef(TEST). | |||||
parse_test() -> | |||||
?assertEqual({ok, [{minute, 0}]}, parse_rotation_date_spec("$H0")), | |||||
?assertEqual({ok, [{minute, 59}]}, parse_rotation_date_spec("$H59")), | |||||
?assertEqual({ok, [{hour, 0}]}, parse_rotation_date_spec("$D0")), | |||||
?assertEqual({ok, [{hour, 23}]}, parse_rotation_date_spec("$D23")), | |||||
?assertEqual({ok, [{day, 0}, {hour, 23}]}, parse_rotation_date_spec("$W0D23")), | |||||
?assertEqual({ok, [{day, 5}, {hour, 16}]}, parse_rotation_date_spec("$W5D16")), | |||||
?assertEqual({ok, [{day, 0}, {hour, 12}, {minute, 30}]}, parse_rotation_date_spec("$W0D12H30")), | |||||
?assertEqual({ok, [{date, 1}, {hour, 0}]}, parse_rotation_date_spec("$M1D0")), | |||||
?assertEqual({ok, [{date, 5}, {hour, 6}]}, parse_rotation_date_spec("$M5D6")), | |||||
?assertEqual({ok, [{date, 5}, {hour, 0}]}, parse_rotation_date_spec("$M5")), | |||||
?assertEqual({ok, [{date, 31}, {hour, 0}]}, parse_rotation_date_spec("$M31")), | |||||
?assertEqual({ok, [{date, 31}, {hour, 1}]}, parse_rotation_date_spec("$M31D1")), | |||||
?assertEqual({ok, [{date, last}, {hour, 0}]}, parse_rotation_date_spec("$ML")), | |||||
?assertEqual({ok, [{date, last}, {hour, 0}]}, parse_rotation_date_spec("$Ml")), | |||||
?assertEqual({ok, [{day, 5}, {hour, 0}]}, parse_rotation_date_spec("$W5")), | |||||
ok. | |||||
parse_fail_test() -> | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$H")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$H60")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$D")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$D24")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$W7")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$W7D1")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$M32")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$M32D1")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$D15M5")), | |||||
?assertEqual({error, invalid_date_spec}, parse_rotation_date_spec("$M5W5")), | |||||
ok. | |||||
rotation_calculation_test() -> | |||||
?assertMatch({{2000, 1, 1}, {13, 0, 0}}, | |||||
calculate_next_rotation([{minute, 0}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 1}, {12, 45, 0}}, | |||||
calculate_next_rotation([{minute, 45}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 2}, {0, 0, 0}}, | |||||
calculate_next_rotation([{minute, 0}], {{2000, 1, 1}, {23, 45, 43}})), | |||||
?assertMatch({{2000, 1, 2}, {0, 0, 0}}, | |||||
calculate_next_rotation([{hour, 0}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 1}, {16, 0, 0}}, | |||||
calculate_next_rotation([{hour, 16}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 2}, {12, 0, 0}}, | |||||
calculate_next_rotation([{hour, 12}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 2, 1}, {12, 0, 0}}, | |||||
calculate_next_rotation([{date, 1}, {hour, 12}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 2, 1}, {12, 0, 0}}, | |||||
calculate_next_rotation([{date, 1}, {hour, 12}], {{2000, 1, 15}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 2, 1}, {12, 0, 0}}, | |||||
calculate_next_rotation([{date, 1}, {hour, 12}], {{2000, 1, 2}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 2, 1}, {12, 0, 0}}, | |||||
calculate_next_rotation([{date, 1}, {hour, 12}], {{2000, 1, 31}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 1}, {16, 0, 0}}, | |||||
calculate_next_rotation([{date, 1}, {hour, 16}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 15}, {16, 0, 0}}, | |||||
calculate_next_rotation([{date, 15}, {hour, 16}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 31}, {16, 0, 0}}, | |||||
calculate_next_rotation([{date, last}, {hour, 16}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 31}, {16, 0, 0}}, | |||||
calculate_next_rotation([{date, last}, {hour, 16}], {{2000, 1, 31}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 2, 29}, {16, 0, 0}}, | |||||
calculate_next_rotation([{date, last}, {hour, 16}], {{2000, 1, 31}, {17, 34, 43}})), | |||||
?assertMatch({{2001, 2, 28}, {16, 0, 0}}, | |||||
calculate_next_rotation([{date, last}, {hour, 16}], {{2001, 1, 31}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 1}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 6}, {hour, 16}], {{2000, 1, 1}, {12, 34, 43}})), | |||||
?assertMatch({{2000, 1, 8}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 6}, {hour, 16}], {{2000, 1, 1}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 7}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 5}, {hour, 16}], {{2000, 1, 1}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 3}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 1}, {hour, 16}], {{2000, 1, 1}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 2}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 0}, {hour, 16}], {{2000, 1, 1}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 9}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 0}, {hour, 16}], {{2000, 1, 2}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 2, 3}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 4}, {hour, 16}], {{2000, 1, 29}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 7}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 5}, {hour, 16}], {{2000, 1, 3}, {17, 34, 43}})), | |||||
?assertMatch({{2000, 1, 3}, {16, 0, 0}}, | |||||
calculate_next_rotation([{day, 1}, {hour, 16}], {{1999, 12, 28}, {17, 34, 43}})), | |||||
ok. | |||||
check_trace_test() -> | |||||
lager:start(), | |||||
trace_filter(none), | |||||
%% match by module | |||||
?assertEqual([foo], check_traces([{module, ?MODULE}], ?EMERGENCY, [ | |||||
{[{module, ?MODULE}], config_to_mask(emergency), foo}, | |||||
{[{module, test}], config_to_mask(emergency), bar}], [])), | |||||
%% match by module, but other unsatisfyable attribute | |||||
?assertEqual([], check_traces([{module, ?MODULE}], ?EMERGENCY, [ | |||||
{[{module, ?MODULE}, {foo, bar}], config_to_mask(emergency), foo}, | |||||
{[{module, test}], config_to_mask(emergency), bar}], [])), | |||||
%% match by wildcard module | |||||
?assertEqual([bar], check_traces([{module, ?MODULE}], ?EMERGENCY, [ | |||||
{[{module, ?MODULE}, {foo, bar}], config_to_mask(emergency), foo}, | |||||
{[{module, '*'}], config_to_mask(emergency), bar}], [])), | |||||
%% wildcard module, one trace with unsatisfyable attribute | |||||
?assertEqual([bar], check_traces([{module, ?MODULE}], ?EMERGENCY, [ | |||||
{[{module, '*'}, {foo, bar}], config_to_mask(emergency), foo}, | |||||
{[{module, '*'}], config_to_mask(emergency), bar}], [])), | |||||
%% wildcard but not present custom trace attribute | |||||
?assertEqual([bar], check_traces([{module, ?MODULE}], ?EMERGENCY, [ | |||||
{[{module, '*'}, {foo, '*'}], config_to_mask(emergency), foo}, | |||||
{[{module, '*'}], config_to_mask(emergency), bar}], [])), | |||||
%% wildcarding a custom attribute works when it is present | |||||
?assertEqual([bar, foo], check_traces([{module, ?MODULE}, {foo, bar}], ?EMERGENCY, [ | |||||
{[{module, '*'}, {foo, '*'}], config_to_mask(emergency), foo}, | |||||
{[{module, '*'}], config_to_mask(emergency), bar}], [])), | |||||
%% denied by level | |||||
?assertEqual([], check_traces([{module, ?MODULE}, {foo, bar}], ?INFO, [ | |||||
{[{module, '*'}, {foo, '*'}], config_to_mask(emergency), foo}, | |||||
{[{module, '*'}], config_to_mask(emergency), bar}], [])), | |||||
%% allowed by level | |||||
?assertEqual([foo], check_traces([{module, ?MODULE}, {foo, bar}], ?INFO, [ | |||||
{[{module, '*'}, {foo, '*'}], config_to_mask(debug), foo}, | |||||
{[{module, '*'}], config_to_mask(emergency), bar}], [])), | |||||
?assertEqual([anythingbutnotice, infoandbelow, infoonly], check_traces([{module, ?MODULE}], ?INFO, [ | |||||
{[{module, '*'}], config_to_mask('=debug'), debugonly}, | |||||
{[{module, '*'}], config_to_mask('=info'), infoonly}, | |||||
{[{module, '*'}], config_to_mask('<=info'), infoandbelow}, | |||||
{[{module, '*'}], config_to_mask('!=info'), anythingbutinfo}, | |||||
{[{module, '*'}], config_to_mask('!=notice'), anythingbutnotice} | |||||
], [])), | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
ok. | |||||
is_loggable_test_() -> | |||||
[ | |||||
{"Loggable by severity only", ?_assert(is_loggable(lager_msg:new("", alert, [], []),2,me))}, | |||||
{"Not loggable by severity only", ?_assertNot(is_loggable(lager_msg:new("", critical, [], []),1,me))}, | |||||
{"Loggable by severity with destination", ?_assert(is_loggable(lager_msg:new("", alert, [], [you]),2,me))}, | |||||
{"Not loggable by severity with destination", ?_assertNot(is_loggable(lager_msg:new("", critical, [], [you]),1,me))}, | |||||
{"Loggable by destination overriding severity", ?_assert(is_loggable(lager_msg:new("", critical, [], [me]),1,me))} | |||||
]. | |||||
format_time_test_() -> | |||||
[ | |||||
?_assertEqual("2012-10-04 11:16:23.002", | |||||
begin | |||||
{D, T} = format_time({{2012,10,04},{11,16,23,2}}), | |||||
lists:flatten([D,$ ,T]) | |||||
end), | |||||
?_assertEqual("2012-10-04 11:16:23.999", | |||||
begin | |||||
{D, T} = format_time({{2012,10,04},{11,16,23,999}}), | |||||
lists:flatten([D,$ ,T]) | |||||
end), | |||||
?_assertEqual("2012-10-04 11:16:23", | |||||
begin | |||||
{D, T} = format_time({{2012,10,04},{11,16,23}}), | |||||
lists:flatten([D,$ ,T]) | |||||
end), | |||||
?_assertEqual("2012-10-04 00:16:23.092 UTC", | |||||
begin | |||||
{D, T} = format_time({utc, {{2012,10,04},{0,16,23,92}}}), | |||||
lists:flatten([D,$ ,T]) | |||||
end), | |||||
?_assertEqual("2012-10-04 11:16:23 UTC", | |||||
begin | |||||
{D, T} = format_time({utc, {{2012,10,04},{11,16,23}}}), | |||||
lists:flatten([D,$ ,T]) | |||||
end) | |||||
]. | |||||
config_to_levels_test() -> | |||||
?assertEqual([none], config_to_levels('none')), | |||||
?assertEqual({mask, 0}, config_to_mask('none')), | |||||
?assertEqual([debug], config_to_levels('=debug')), | |||||
?assertEqual([debug], config_to_levels('<info')), | |||||
?assertEqual(levels() -- [debug], config_to_levels('!=debug')), | |||||
?assertEqual(levels() -- [debug], config_to_levels('>debug')), | |||||
?assertEqual(levels() -- [debug], config_to_levels('>=info')), | |||||
?assertEqual(levels() -- [debug], config_to_levels('=>info')), | |||||
?assertEqual([debug, info, notice], config_to_levels('<=notice')), | |||||
?assertEqual([debug, info, notice], config_to_levels('=<notice')), | |||||
?assertEqual([debug], config_to_levels('<info')), | |||||
?assertEqual([debug], config_to_levels('!info')), | |||||
?assertError(badarg, config_to_levels(ok)), | |||||
?assertError(badarg, config_to_levels('<=>info')), | |||||
?assertError(badarg, config_to_levels('=<=info')), | |||||
?assertError(badarg, config_to_levels('<==>=<=>info')), | |||||
%% double negatives DO work, however | |||||
?assertEqual([debug], config_to_levels('!!=debug')), | |||||
?assertEqual(levels() -- [debug], config_to_levels('!!!=debug')), | |||||
ok. | |||||
config_to_mask_test() -> | |||||
?assertEqual({mask, 0}, config_to_mask('none')), | |||||
?assertEqual({mask, ?DEBUG bor ?INFO bor ?NOTICE bor ?WARNING bor ?ERROR bor ?CRITICAL bor ?ALERT bor ?EMERGENCY}, config_to_mask('debug')), | |||||
?assertEqual({mask, ?WARNING bor ?ERROR bor ?CRITICAL bor ?ALERT bor ?EMERGENCY}, config_to_mask('warning')), | |||||
?assertEqual({mask, ?DEBUG bor ?NOTICE bor ?WARNING bor ?ERROR bor ?CRITICAL bor ?ALERT bor ?EMERGENCY}, config_to_mask('!=info')), | |||||
ok. | |||||
mask_to_levels_test() -> | |||||
?assertEqual([], mask_to_levels(0)), | |||||
?assertEqual([debug], mask_to_levels(2#10000000)), | |||||
?assertEqual([debug, info], mask_to_levels(2#11000000)), | |||||
?assertEqual([debug, info, emergency], mask_to_levels(2#11000001)), | |||||
?assertEqual([debug, notice, error], mask_to_levels(?DEBUG bor ?NOTICE bor ?ERROR)), | |||||
ok. | |||||
expand_path_test() -> | |||||
OldRootVal = application:get_env(lager, log_root), | |||||
ok = application:unset_env(lager, log_root), | |||||
?assertEqual("/foo/bar", expand_path("/foo/bar")), | |||||
?assertEqual("foo/bar", expand_path("foo/bar")), | |||||
ok = application:set_env(lager, log_root, "log/dir"), | |||||
?assertEqual("/foo/bar", expand_path("/foo/bar")), % Absolute path should not be changed | |||||
?assertEqual("log/dir/foo/bar", expand_path("foo/bar")), | |||||
?assertEqual("log/dir/foo/bar", expand_path("log/dir/foo/bar")), %% gh #304 | |||||
case OldRootVal of | |||||
undefined -> application:unset_env(lager, log_root); | |||||
{ok, Root} -> application:set_env(lager, log_root, Root) | |||||
end, | |||||
ok. | |||||
sink_name_test_() -> | |||||
[ | |||||
?_assertEqual(lager_event, make_internal_sink_name(lager)), | |||||
?_assertEqual(audit_lager_event, make_internal_sink_name(audit)) | |||||
]. | |||||
create_test_dir() -> | |||||
{ok, Tmp} = get_temp_dir(), | |||||
Dir = filename:join([Tmp, "lager_test", | |||||
erlang:integer_to_list(erlang:phash2(os:timestamp()))]), | |||||
?assertEqual(ok, filelib:ensure_dir(Dir)), | |||||
TestDir = case file:make_dir(Dir) of | |||||
ok -> | |||||
Dir; | |||||
Err -> | |||||
?assertEqual({error, eexist}, Err), | |||||
create_test_dir() | |||||
end, | |||||
ok = application:set_env(lager, test_dir, TestDir), | |||||
{ok, TestDir}. | |||||
get_test_dir() -> | |||||
case application:get_env(lager, test_dir) of | |||||
undefined -> | |||||
create_test_dir(); | |||||
{ok, _}=Res -> | |||||
Res | |||||
end. | |||||
get_temp_dir() -> | |||||
Tmp = case os:getenv("TEMP") of | |||||
false -> | |||||
case os:getenv("TMP") of | |||||
false -> "/tmp"; | |||||
Dir1 -> Dir1 | |||||
end; | |||||
Dir0 -> Dir0 | |||||
end, | |||||
?assertEqual(true, filelib:is_dir(Tmp)), | |||||
{ok, Tmp}. | |||||
delete_test_dir() -> | |||||
{ok, TestDir} = get_test_dir(), | |||||
ok = delete_test_dir(TestDir). | |||||
delete_test_dir(TestDir) -> | |||||
ok = application:unset_env(lager, test_dir), | |||||
{OsType, _} = os:type(), | |||||
ok = case {OsType, otp_version()} of | |||||
{win32, _} -> | |||||
application:stop(lager), | |||||
do_delete_test_dir(TestDir); | |||||
{unix, 15} -> | |||||
os:cmd("rm -rf " ++ TestDir); | |||||
{unix, _} -> | |||||
do_delete_test_dir(TestDir) | |||||
end. | |||||
do_delete_test_dir(Dir) -> | |||||
ListRet = file:list_dir_all(Dir), | |||||
?assertMatch({ok, _}, ListRet), | |||||
{_, Entries} = ListRet, | |||||
lists:foreach( | |||||
fun(Entry) -> | |||||
FsElem = filename:join(Dir, Entry), | |||||
case filelib:is_dir(FsElem) of | |||||
true -> | |||||
delete_test_dir(FsElem); | |||||
_ -> | |||||
case file:delete(FsElem) of | |||||
ok -> ok; | |||||
Error -> | |||||
io:format(standard_error, "[ERROR]: error deleting file ~p~n", [FsElem]), | |||||
?assertEqual(ok, Error) | |||||
end | |||||
end | |||||
end, Entries), | |||||
?assertEqual(ok, file:del_dir(Dir)). | |||||
do_delete_file(_FsElem, 0) -> | |||||
?assert(false); | |||||
do_delete_file(FsElem, Attempts) -> | |||||
case file:delete(FsElem) of | |||||
ok -> ok; | |||||
_Error -> | |||||
do_delete_file(FsElem, Attempts - 1) | |||||
end. | |||||
set_dir_permissions(Perms, Dir) -> | |||||
do_set_dir_permissions(os:type(), Perms, Dir). | |||||
do_set_dir_permissions({win32, _}, _Perms, _Dir) -> | |||||
ok; | |||||
do_set_dir_permissions({unix, _}, Perms, Dir) -> | |||||
os:cmd("chmod -R " ++ Perms ++ " " ++ Dir), | |||||
ok. | |||||
safe_application_load(App) -> | |||||
case application:load(App) of | |||||
ok -> | |||||
ok; | |||||
{error, {already_loaded, App}} -> | |||||
ok; | |||||
Error -> | |||||
?assertEqual(ok, Error) | |||||
end. | |||||
safe_write_file(File, Content) -> | |||||
% Note: ensures that the new creation time is at least one second | |||||
% in the future | |||||
?assertEqual(ok, file:write_file(File, Content)), | |||||
Ctime0 = calendar:local_time(), | |||||
Ctime0Sec = calendar:datetime_to_gregorian_seconds(Ctime0), | |||||
Ctime1Sec = Ctime0Sec + 1, | |||||
Ctime1 = calendar:gregorian_seconds_to_datetime(Ctime1Sec), | |||||
{ok, FInfo0} = file:read_file_info(File, [raw]), | |||||
FInfo1 = FInfo0#file_info{ctime = Ctime1}, | |||||
?assertEqual(ok, file:write_file_info(File, FInfo1, [raw])). | |||||
-endif. |
@ -0,0 +1,22 @@ | |||||
-module(compress_pr_record_test). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-record(a, {field1 :: term(), | |||||
field2 :: term(), | |||||
foo :: term(), | |||||
bar :: term(), | |||||
baz :: term(), | |||||
zyu :: term(), | |||||
zix :: term()}). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
nested_record_test() -> | |||||
A = #a{field1 = "Notice me senpai"}, | |||||
Pr_A = lager:pr(A, ?MODULE), | |||||
Pr_A_Comp = lager:pr(A, ?MODULE, [compress]), | |||||
?assertMatch({'$lager_record', a, [{field1, "Notice me senpai"}, {field2, undefined} | _]}, Pr_A), | |||||
?assertEqual({'$lager_record', a, [{field1, "Notice me senpai"}]}, Pr_A_Comp). |
@ -0,0 +1,109 @@ | |||||
%% a module that crashes in just about every way possible | |||||
-module(crash). | |||||
-behaviour(gen_server). | |||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). | |||||
-export([start/0]). | |||||
-record(state, { | |||||
host :: term(), | |||||
port :: term() | |||||
}). | |||||
start() -> | |||||
gen_server:start({local, ?MODULE}, ?MODULE, [], []). | |||||
init(_) -> | |||||
{ok, {}}. | |||||
handle_call(undef, _, State) -> | |||||
{reply, ?MODULE:booger(), State}; | |||||
handle_call(badfun, _, State) -> | |||||
M = booger, | |||||
{reply, M(), State}; | |||||
handle_call(bad_return, _, _) -> | |||||
bleh; | |||||
handle_call(bad_return_string, _, _) -> | |||||
{tuple, {tuple, "string"}}; | |||||
handle_call(case_clause, _, State) -> | |||||
case State of | |||||
goober -> | |||||
{reply, ok, State} | |||||
end; | |||||
handle_call(case_clause_string, _, State) -> | |||||
Foo = atom_to_list(?MODULE), | |||||
case Foo of | |||||
State -> | |||||
{reply, ok, State} | |||||
end; | |||||
handle_call(if_clause, _, State) -> | |||||
if State == 1 -> | |||||
{reply, ok, State} | |||||
end; | |||||
handle_call(try_clause, _, State) -> | |||||
Res = try tuple_to_list(State) of | |||||
[_A, _B] -> ok | |||||
catch | |||||
_:_ -> ok | |||||
end, | |||||
{reply, Res, State}; | |||||
handle_call(badmatch, _, State) -> | |||||
{A, B, C} = State, | |||||
{reply, [A, B, C], State}; | |||||
handle_call(badrecord, _, State) -> | |||||
Host = State#state.host, | |||||
{reply, Host, State}; | |||||
handle_call(function_clause, _, State) -> | |||||
{reply, function(State), State}; | |||||
handle_call(badarith, _, State) -> | |||||
Res = 1 / length(tuple_to_list(State)), | |||||
{reply, Res, State}; | |||||
handle_call(badarg1, _, State) -> | |||||
Res = list_to_binary(["foo", bar]), | |||||
{reply, Res, State}; | |||||
handle_call(badarg2, _, State) -> | |||||
Res = erlang:iolist_to_binary(["foo", bar]), | |||||
{reply, Res, State}; | |||||
handle_call(system_limit, _, State) -> | |||||
Res = list_to_atom(lists:flatten(lists:duplicate(256, "a"))), | |||||
{reply, Res, State}; | |||||
handle_call(process_limit, _, State) -> | |||||
%% run with +P 300 to make this crash | |||||
[erlang:spawn(fun() -> timer:sleep(5000) end) || _ <- lists:seq(0, 500)], | |||||
{reply, ok, State}; | |||||
handle_call(port_limit, _, State) -> | |||||
[erlang:open_port({spawn, "ls"}, []) || _ <- lists:seq(0, 1024)], | |||||
{reply, ok, State}; | |||||
handle_call(noproc, _, State) -> | |||||
Res = gen_event:call(foo, bar, baz), | |||||
{reply, Res, State}; | |||||
handle_call(noproc_proc_lib, _, State) -> | |||||
Res = proc_lib:stop(foo), | |||||
{reply, Res, State}; | |||||
handle_call(badarity, _, State) -> | |||||
F = fun(A, B, C) -> A + B + C end, | |||||
Res = F(State), | |||||
{reply, Res, State}; | |||||
handle_call(throw, _, _State) -> | |||||
throw(a_ball); | |||||
handle_call(_Call, _From, State) -> | |||||
{reply, ok, State}. | |||||
handle_cast(_Cast, State) -> | |||||
{noreply, State}. | |||||
handle_info(_Info, State) -> | |||||
{noreply, State}. | |||||
terminate(_, _) -> | |||||
ok. | |||||
code_change(_, State, _) -> | |||||
{ok, State}. | |||||
function(X) when is_list(X) -> | |||||
ok. |
@ -0,0 +1,35 @@ | |||||
-module(crash_fsm). | |||||
-behaviour(gen_fsm). | |||||
-compile([{nowarn_deprecated_function, [{gen_fsm, start, 4}, {gen_fsm, sync_send_event, 2}]}]). | |||||
-export([start/0, crash/0, state1/2]). | |||||
%% gen_fsm callbacks | |||||
-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, | |||||
terminate/3, code_change/4]). | |||||
-record(state, {}). | |||||
start() -> | |||||
gen_fsm:start({local, ?MODULE}, ?MODULE, [], []). | |||||
crash() -> | |||||
gen_fsm:sync_send_event(?MODULE, crash). | |||||
%% gen_fsm callbacks | |||||
init([]) -> | |||||
{ok, state1, #state{}}. | |||||
handle_event(_Event, StateName, State) -> | |||||
{next_state, StateName, State}. | |||||
handle_sync_event(_Event, _From, StateName, State) -> | |||||
Reply = ok, | |||||
{reply, Reply, StateName, State}. | |||||
handle_info(_Info, StateName, State) -> | |||||
{next_state, StateName, State}. | |||||
terminate(_Reason, _StateName, _State) -> | |||||
ok. | |||||
code_change(_OldVersion, StateName, State, _Extra) -> | |||||
{ok, StateName, State}. | |||||
state1(_Event, S) -> {next_state, state1, S}. |
@ -0,0 +1,55 @@ | |||||
-module(crash_statem). | |||||
%% we're only going to compile this on OTP 19+ | |||||
-ifdef(test_statem). | |||||
-behaviour(gen_statem). | |||||
-export([ | |||||
start/0, | |||||
crash/0, | |||||
stop/1, | |||||
timeout/0, | |||||
handle_event/4 | |||||
]). | |||||
-export([terminate/3,code_change/4,init/1,callback_mode/0]). | |||||
start() -> | |||||
gen_statem:start({local,?MODULE}, ?MODULE, [], []). | |||||
crash() -> | |||||
gen_statem:call(?MODULE, boom). | |||||
stop(Reason) -> | |||||
gen_statem:call(?MODULE, {stop, Reason}). | |||||
timeout() -> | |||||
gen_statem:call(?MODULE, timeout). | |||||
%% Mandatory callback functions | |||||
terminate(_Reason, _State, _Data) -> ok. | |||||
code_change(_Vsn, State, Data, _Extra) -> {ok,State,Data}. | |||||
init([]) -> | |||||
%% insert rant here about breaking changes in minor versions... | |||||
case erlang:system_info(version) of | |||||
"8.0" -> {callback_mode(),state1,undefined}; | |||||
_ -> {ok, state1, undefined} | |||||
end. | |||||
callback_mode() -> handle_event_function. | |||||
%%% state callback(s) | |||||
handle_event(state_timeout, timeout, state1, _) -> | |||||
{stop, timeout}; | |||||
handle_event({call, _From}, timeout, _Arg, _Data) -> | |||||
{keep_state_and_data, [{state_timeout, 0, timeout}]}; | |||||
handle_event({call, _From}, {stop, Reason}, state1, _Data) -> | |||||
{stop, Reason}. | |||||
-else. | |||||
-export([start/0, crash/0]). | |||||
start() -> ok. | |||||
crash() -> ok. | |||||
-endif. |
@ -0,0 +1,15 @@ | |||||
-module(lager_app_tests). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
get_env_test() -> | |||||
application:set_env(myapp, mykey1, <<"Value">>), | |||||
?assertEqual(<<"Some">>, lager_app:get_env(myapp, mykey0, <<"Some">>)), | |||||
?assertEqual(<<"Value">>, lager_app:get_env(myapp, mykey1, <<"Some">>)), | |||||
ok. | |||||
@ -0,0 +1,68 @@ | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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. | |||||
-module(lager_crash_backend). | |||||
-include("lager.hrl"). | |||||
-behaviour(gen_event). | |||||
-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, | |||||
code_change/3]). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
init([CrashBefore, CrashAfter]) -> | |||||
case is_tuple(CrashBefore) andalso (timer:now_diff(CrashBefore, os:timestamp()) > 0) of | |||||
true -> | |||||
%?debugFmt("crashing!~n", []), | |||||
{error, crashed}; | |||||
_ -> | |||||
%?debugFmt("Not crashing!~n", []), | |||||
case is_tuple(CrashAfter) of | |||||
true -> | |||||
CrashTime = timer:now_diff(CrashAfter, os:timestamp()) div 1000, | |||||
case CrashTime > 0 of | |||||
true -> | |||||
%?debugFmt("crashing in ~p~n", [CrashTime]), | |||||
erlang:send_after(CrashTime, self(), crash), | |||||
{ok, {}}; | |||||
_ -> {error, crashed} | |||||
end; | |||||
_ -> | |||||
{ok, {}} | |||||
end | |||||
end. | |||||
handle_call(_Request, State) -> | |||||
{ok, ok, State}. | |||||
handle_event(_Event, State) -> | |||||
{ok, State}. | |||||
handle_info(crash, _State) -> | |||||
%?debugFmt("Time to crash!~n", []), | |||||
crash; | |||||
handle_info(_Info, State) -> | |||||
{ok, State}. | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. |
@ -0,0 +1,125 @@ | |||||
-module(lager_manager_killer_test). | |||||
-author("Sungjin Park <jinni.park@gmail.com>"). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-define(TEST_SINK_NAME, '__lager_test_sink'). %% <-- used by parse transform | |||||
-define(TEST_SINK_EVENT, '__lager_test_sink_lager_event'). %% <-- used by lager API calls and internals for gen_event | |||||
overload_test_() -> | |||||
{timeout, 60, | |||||
fun() -> | |||||
application:stop(lager), | |||||
application:load(lager), | |||||
Delay = 1000, % sleep 1 sec on every log | |||||
KillerHWM = 10, % kill the manager if there are more than 10 pending logs | |||||
KillerReinstallAfter = 1000, % reinstall killer after 1 sec | |||||
application:set_env(lager, handlers, [{lager_slow_backend, [{delay, Delay}]}]), | |||||
application:set_env(lager, async_threshold, undefined), | |||||
application:set_env(lager, error_logger_redirect, true), | |||||
application:set_env(lager, killer_hwm, KillerHWM), | |||||
application:set_env(lager, killer_reinstall_after, KillerReinstallAfter), | |||||
ensure_started(lager), | |||||
lager_config:set(async, true), | |||||
Manager = whereis(lager_event), | |||||
erlang:trace(all, true, [procs]), | |||||
[lager:info("~p'th message", [N]) || N <- lists:seq(1,KillerHWM+2)], | |||||
Margin = 100, | |||||
ok = confirm_manager_exit(Manager, Delay+Margin), | |||||
ok = confirm_sink_reregister(lager_event, Margin), | |||||
erlang:trace(all, false, [procs]), | |||||
wait_until(fun() -> | |||||
case proplists:get_value(lager_manager_killer, gen_event:which_handlers(lager_event)) of | |||||
[] -> false; | |||||
_ -> true | |||||
end | |||||
end, Margin, 15), | |||||
wait_until(fun() -> | |||||
case gen_event:call(lager_event, lager_manager_killer, get_settings) of | |||||
[KillerHWM, KillerReinstallAfter] -> true; | |||||
_Other -> false | |||||
end | |||||
end, Margin, 15), | |||||
application:stop(lager) | |||||
end}. | |||||
overload_alternate_sink_test_() -> | |||||
{timeout, 60, | |||||
fun() -> | |||||
application:stop(lager), | |||||
application:load(lager), | |||||
Delay = 1000, % sleep 1 sec on every log | |||||
KillerHWM = 10, % kill the manager if there are more than 10 pending logs | |||||
KillerReinstallAfter = 1000, % reinstall killer after 1 sec | |||||
application:set_env(lager, handlers, []), | |||||
application:set_env(lager, extra_sinks, [{?TEST_SINK_EVENT, [ | |||||
{handlers, [{lager_slow_backend, [{delay, Delay}]}]}, | |||||
{killer_hwm, KillerHWM}, | |||||
{killer_reinstall_after, KillerReinstallAfter}, | |||||
{async_threshold, undefined} | |||||
]}]), | |||||
application:set_env(lager, error_logger_redirect, true), | |||||
ensure_started(lager), | |||||
lager_config:set({?TEST_SINK_EVENT, async}, true), | |||||
Manager = whereis(?TEST_SINK_EVENT), | |||||
erlang:trace(all, true, [procs]), | |||||
[?TEST_SINK_NAME:info("~p'th message", [N]) || N <- lists:seq(1,KillerHWM+2)], | |||||
Margin = 100, | |||||
ok = confirm_manager_exit(Manager, Delay+Margin), | |||||
ok = confirm_sink_reregister(?TEST_SINK_EVENT, Margin), | |||||
erlang:trace(all, false, [procs]), | |||||
wait_until(fun() -> | |||||
case proplists:get_value(lager_manager_killer, gen_event:which_handlers(?TEST_SINK_EVENT)) of | |||||
[] -> false; | |||||
_ -> true | |||||
end | |||||
end, Margin, 15), | |||||
wait_until(fun() -> | |||||
case gen_event:call(?TEST_SINK_EVENT, lager_manager_killer, get_settings) of | |||||
[KillerHWM, KillerReinstallAfter] -> true; | |||||
_Other -> false | |||||
end | |||||
end, Margin, 15), | |||||
application:stop(lager) | |||||
end}. | |||||
ensure_started(App) -> | |||||
case application:start(App) of | |||||
ok -> | |||||
ok; | |||||
{error, {not_started, Dep}} -> | |||||
ensure_started(Dep), | |||||
ensure_started(App) | |||||
end. | |||||
confirm_manager_exit(Manager, Delay) -> | |||||
receive | |||||
{trace, Manager, exit, killed} -> | |||||
?debugFmt("Manager ~p killed", [Manager]); | |||||
Other -> | |||||
?debugFmt("OTHER MSG: ~p", [Other]), | |||||
confirm_manager_exit(Manager, Delay) | |||||
after Delay -> | |||||
?assert(false) | |||||
end. | |||||
confirm_sink_reregister(Sink, Delay) -> | |||||
receive | |||||
{trace, _Pid, register, Sink} -> | |||||
?assertNot(lists:member(lager_manager_killer, gen_event:which_handlers(Sink))) | |||||
after Delay -> | |||||
?assert(false) | |||||
end. | |||||
wait_until(_Fun, _Delay, 0) -> | |||||
{error, too_many_retries}; | |||||
wait_until(Fun, Delay, Retries) -> | |||||
case Fun() of | |||||
true -> ok; | |||||
false -> timer:sleep(Delay), wait_until(Fun, Delay, Retries-1) | |||||
end. | |||||
-endif. |
@ -0,0 +1,72 @@ | |||||
-module(lager_metadata_whitelist_test). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
setup() -> | |||||
ok = error_logger:tty(false), | |||||
ok = lager_util:safe_application_load(lager), | |||||
ok = application:set_env(lager, handlers, [{lager_common_test_backend, info}]), | |||||
ok = application:set_env(lager, error_logger_redirect, false), | |||||
ok = application:unset_env(lager, traces), | |||||
ok = lager:start(), | |||||
ok = timer:sleep(250), | |||||
ok. | |||||
cleanup(_) -> | |||||
ok = application:unset_env(lager, metadata_whitelist), | |||||
catch ets:delete(lager_config), %% kill the ets config table with fire | |||||
ok = application:stop(lager), | |||||
ok = application:stop(goldrush), | |||||
ok = error_logger:tty(true). | |||||
date_time_now() -> | |||||
Now = os:timestamp(), | |||||
{Date, Time} = lager_util:format_time(lager_util:maybe_utc(lager_util:localtime_ms(Now))), | |||||
{Date, Time, Now}. | |||||
basic_test_() -> | |||||
{Date, Time, Now} = date_time_now(), | |||||
{ | |||||
foreach, | |||||
fun setup/0, | |||||
fun cleanup/1, | |||||
[{"Meta", fun() -> | |||||
Whitelist = [meta0], | |||||
ok = application:set_env(lager, metadata_whitelist, Whitelist), | |||||
Msg = lager_msg:new("Message", Now, error, [], []), | |||||
Expected = iolist_to_binary([Date, " ", Time, " [error] Message\n"]), | |||||
Got = iolist_to_binary(lager_default_formatter:format(Msg, [])), | |||||
?assertEqual(Expected, Got) | |||||
end}, | |||||
{"Meta1", fun() -> | |||||
Whitelist = [meta1], | |||||
ok = application:set_env(lager, metadata_whitelist, Whitelist), | |||||
Msg = lager_msg:new("Message", Now, error, [{meta1, "value1"}], []), | |||||
Expected = iolist_to_binary([Date, " ", Time, " [error] meta1=value1 Message\n"]), | |||||
Got = iolist_to_binary(lager_default_formatter:format(Msg, [])), | |||||
?assertEqual(Expected, Got) | |||||
end}, | |||||
{"Meta2", fun() -> | |||||
Whitelist = [meta1, meta2], | |||||
ok = application:set_env(lager, metadata_whitelist, Whitelist), | |||||
Msg = lager_msg:new("Message", Now, error, [{meta1, "value1"}, {meta2, 2}], []), | |||||
Expected = iolist_to_binary([Date, " ", Time, " [error] meta1=value1 meta2=2 Message\n"]), | |||||
Got = iolist_to_binary(lager_default_formatter:format(Msg, [])), | |||||
?assertEqual(Expected, Got) | |||||
end}, | |||||
{"Meta3", fun() -> | |||||
Whitelist = [meta1, meta2], | |||||
ok = application:set_env(lager, metadata_whitelist, Whitelist), | |||||
Msg = lager_msg:new("Message", Now, error, [{meta1, "value1"}, {meta3, 3}], []), | |||||
Expected = iolist_to_binary([Date, " ", Time, " [error] meta1=value1 Message\n"]), | |||||
Got = iolist_to_binary(lager_default_formatter:format(Msg, [])), | |||||
?assertEqual(Expected, Got) | |||||
end} | |||||
] | |||||
}. | |||||
-endif. |
@ -0,0 +1,185 @@ | |||||
%% ------------------------------------------------------------------- | |||||
%% | |||||
%% Copyright (c) 2016-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. | |||||
%% | |||||
%% ------------------------------------------------------------------- | |||||
-module(lager_rotate). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-endif. | |||||
-record(state, { | |||||
dir :: string(), | |||||
log1 :: string(), | |||||
log1r :: string(), | |||||
log2 :: string(), | |||||
log2r :: string(), | |||||
sink :: string(), | |||||
sinkr :: string() | |||||
}). | |||||
rotate_test_() -> | |||||
{foreach, | |||||
fun() -> | |||||
{ok, Dir} = lager_util:create_test_dir(), | |||||
Log1 = filename:join(Dir, "test1.log"), | |||||
Log2 = filename:join(Dir, "test2.log"), | |||||
Sink = filename:join(Dir, "sink.log"), | |||||
State = #state{ | |||||
dir = Dir, | |||||
log1 = Log1, | |||||
log1r = Log1 ++ ".0", | |||||
log2 = Log2, | |||||
log2r = Log2 ++ ".0", | |||||
sink = Sink, | |||||
sinkr = Sink ++ ".0" | |||||
}, | |||||
file:write_file(Log1, []), | |||||
file:write_file(Log2, []), | |||||
file:write_file(Sink, []), | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, handlers, [ | |||||
{lager_file_backend, [{file, Log1}, {level, info}]}, | |||||
{lager_file_backend, [{file, Log2}, {level, info}]} ]), | |||||
application:set_env(lager, extra_sinks, [ | |||||
{sink_event, | |||||
[{handlers, | |||||
[{lager_file_backend, [{file, Sink}, {level, info}]}]} | |||||
]}]), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
application:set_env(lager, async_threshold, undefined), | |||||
lager:start(), | |||||
timer:sleep(1000), | |||||
State | |||||
end, | |||||
fun(#state{}) -> | |||||
ok = application:stop(lager), | |||||
ok = application:stop(goldrush), | |||||
ok = lager_util:delete_test_dir(), | |||||
ok = error_logger:tty(true) | |||||
end, [ | |||||
fun(State) -> | |||||
{"Rotate single file", | |||||
fun() -> | |||||
lager:log(error, self(), "Test message 1"), | |||||
lager:log(sink_event, error, self(), "Sink test message 1", []), | |||||
lager:rotate_handler({lager_file_backend, State#state.log1}), | |||||
ok = wait_until(fun() -> filelib:is_regular(State#state.log1r) end, 10), | |||||
lager:log(error, self(), "Test message 2"), | |||||
lager:log(sink_event, error, self(), "Sink test message 2", []), | |||||
{ok, File1} = file:read_file(State#state.log1), | |||||
{ok, File2} = file:read_file(State#state.log2), | |||||
{ok, SinkFile} = file:read_file(State#state.sink), | |||||
{ok, File1Old} = file:read_file(State#state.log1r), | |||||
have_no_log(File1, <<"Test message 1">>), | |||||
have_log(File1, <<"Test message 2">>), | |||||
have_log(File2, <<"Test message 1">>), | |||||
have_log(File2, <<"Test message 2">>), | |||||
have_log(File1Old, <<"Test message 1">>), | |||||
have_no_log(File1Old, <<"Test message 2">>), | |||||
have_log(SinkFile, <<"Sink test message 1">>), | |||||
have_log(SinkFile, <<"Sink test message 2">>) | |||||
end} | |||||
end, | |||||
fun(State) -> | |||||
{"Rotate sink", | |||||
fun() -> | |||||
lager:log(error, self(), "Test message 1"), | |||||
lager:log(sink_event, error, self(), "Sink test message 1", []), | |||||
lager:rotate_sink(sink_event), | |||||
ok = wait_until(fun() -> filelib:is_regular(State#state.sinkr) end, 10), | |||||
lager:log(error, self(), "Test message 2"), | |||||
lager:log(sink_event, error, self(), "Sink test message 2", []), | |||||
{ok, File1} = file:read_file(State#state.log1), | |||||
{ok, File2} = file:read_file(State#state.log2), | |||||
{ok, SinkFile} = file:read_file(State#state.sink), | |||||
{ok, SinkFileOld} = file:read_file(State#state.sinkr), | |||||
have_log(File1, <<"Test message 1">>), | |||||
have_log(File1, <<"Test message 2">>), | |||||
have_log(File2, <<"Test message 1">>), | |||||
have_log(File2, <<"Test message 2">>), | |||||
have_log(SinkFileOld, <<"Sink test message 1">>), | |||||
have_no_log(SinkFileOld, <<"Sink test message 2">>), | |||||
have_no_log(SinkFile, <<"Sink test message 1">>), | |||||
have_log(SinkFile, <<"Sink test message 2">>) | |||||
end} | |||||
end, | |||||
fun(State) -> | |||||
{"Rotate all", | |||||
fun() -> | |||||
lager:log(error, self(), "Test message 1"), | |||||
lager:log(sink_event, error, self(), "Sink test message 1", []), | |||||
lager:rotate_all(), | |||||
ok = wait_until(fun() -> filelib:is_regular(State#state.sinkr) end, 10), | |||||
lager:log(error, self(), "Test message 2"), | |||||
lager:log(sink_event, error, self(), "Sink test message 2", []), | |||||
{ok, File1} = file:read_file(State#state.log1), | |||||
{ok, File2} = file:read_file(State#state.log2), | |||||
{ok, SinkFile} = file:read_file(State#state.sink), | |||||
{ok, File1Old} = file:read_file(State#state.log1r), | |||||
{ok, File2Old} = file:read_file(State#state.log2r), | |||||
{ok, SinkFileOld} = file:read_file(State#state.sinkr), | |||||
have_no_log(File1, <<"Test message 1">>), | |||||
have_log(File1, <<"Test message 2">>), | |||||
have_no_log(File2, <<"Test message 1">>), | |||||
have_log(File2, <<"Test message 2">>), | |||||
have_no_log(SinkFile, <<"Sink test message 1">>), | |||||
have_log(SinkFile, <<"Sink test message 2">>), | |||||
have_log(SinkFileOld, <<"Sink test message 1">>), | |||||
have_no_log(SinkFileOld, <<"Sink test message 2">>), | |||||
have_log(File1Old, <<"Test message 1">>), | |||||
have_no_log(File1Old, <<"Test message 2">>), | |||||
have_log(File2Old, <<"Test message 1">>), | |||||
have_no_log(File2Old, <<"Test message 2">>) | |||||
end} | |||||
end | |||||
]}. | |||||
have_log(Data, Log) -> | |||||
{_,_} = binary:match(Data, Log). | |||||
have_no_log(Data, Log) -> | |||||
nomatch = binary:match(Data, Log). | |||||
wait_until(_Fun, 0) -> {error, too_many_retries}; | |||||
wait_until(Fun, Retry) -> | |||||
case Fun() of | |||||
true -> ok; | |||||
false -> | |||||
timer:sleep(500), | |||||
wait_until(Fun, Retry-1) | |||||
end. |
@ -0,0 +1,34 @@ | |||||
-module(lager_slow_backend). | |||||
-author("Sungjin Park <jinni.park@gmail.com>"). | |||||
-behavior(gen_event). | |||||
-export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, code_change/3]). | |||||
-include("lager.hrl"). | |||||
-record(state, { | |||||
delay :: non_neg_integer() | |||||
}). | |||||
init([{delay, Delay}]) -> | |||||
{ok, #state{delay=Delay}}. | |||||
handle_call(get_loglevel, State) -> | |||||
{ok, lager_util:config_to_mask(debug), State}; | |||||
handle_call(_Request, State) -> | |||||
{ok, ok, State}. | |||||
handle_event({log, _Message}, State) -> | |||||
timer:sleep(State#state.delay), | |||||
{ok, State}; | |||||
handle_event(_Event, State) -> | |||||
{ok, State}. | |||||
handle_info(_Info, State) -> | |||||
{ok, State}. | |||||
terminate(_Reason, _State) -> | |||||
ok. | |||||
code_change(_OldVsn, State, _Extra) -> | |||||
{ok, State}. |
@ -0,0 +1,198 @@ | |||||
%% ------------------------------------------------------------------- | |||||
%% | |||||
%% 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. | |||||
%% | |||||
%% ------------------------------------------------------------------- | |||||
-module(lager_test_function_transform). | |||||
-include("lager.hrl"). | |||||
-compile([{nowarn_deprecated_function, [{erlang, now, 0}]}]). | |||||
-lager_function_transforms([ | |||||
{returns_static_emit, on_emit, {lager_test_function_transform, transform_static}}, | |||||
{returns_dynamic_emit, on_emit, {lager_test_function_transform, transform_dynamic}}, | |||||
{returns_undefined_emit, on_emit, {not_real_module_fake, fake_not_real_function}}, | |||||
{returns_static_log, on_log, {lager_test_function_transform, transform_static}}, | |||||
{returns_dynamic_log, on_log, {lager_test_function_transform, transform_dynamic}} | |||||
]). | |||||
-compile({parse_transform, lager_transform}). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-export([ | |||||
transform_static/0, | |||||
transform_dynamic/0 | |||||
]). | |||||
-endif. | |||||
-ifdef(TEST). | |||||
transform_static() -> | |||||
static_result. | |||||
transform_dynamic() -> | |||||
case lager_util:otp_version() >= 18 of | |||||
true -> | |||||
erlang:monotonic_time(); | |||||
false -> | |||||
erlang:now() | |||||
end. | |||||
not_running_test() -> | |||||
?assertEqual({error, lager_not_running}, lager:log(info, self(), "not running")). | |||||
setup() -> | |||||
ok = error_logger:tty(false), | |||||
ok = lager_util:safe_application_load(lager), | |||||
ok = application:set_env(lager, handlers, [{lager_test_backend, info}]), | |||||
ok = application:set_env(lager, error_logger_redirect, false), | |||||
ok = application:unset_env(lager, traces), | |||||
ok = lager:start(), | |||||
%% There is a race condition between the application start up, lager logging its own | |||||
%% start up condition and several tests that count messages or parse the output of | |||||
%% tests. When the lager start up message wins the race, it causes these tests | |||||
%% which parse output or count message arrivals to fail. | |||||
%% | |||||
%% We introduce a sleep here to allow `flush' to arrive *after* the start up | |||||
%% message has been received and processed. | |||||
%% | |||||
%% This race condition was first exposed during the work on | |||||
%% 4b5260c4524688b545cc12da6baa2dfa4f2afec9 which introduced the lager | |||||
%% manager killer PR. | |||||
ok = timer:sleep(250), | |||||
ok = gen_event:call(lager_event, lager_test_backend, flush). | |||||
cleanup(_) -> | |||||
catch ets:delete(lager_config), %% kill the ets config table with fire | |||||
ok = application:stop(lager), | |||||
ok = application:stop(goldrush), | |||||
ok = error_logger:tty(true). | |||||
transform_function_test_() -> | |||||
{foreach, | |||||
fun setup/0, | |||||
fun cleanup/1, | |||||
[ | |||||
{"observe that there is nothing up my sleeve", | |||||
fun() -> | |||||
?assertEqual(undefined, lager_test_backend:pop()), | |||||
?assertEqual(0, lager_test_backend:count()) | |||||
end | |||||
}, | |||||
{"logging works", | |||||
fun() -> | |||||
lager:warning("test message"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{Level, _Time, Message, _Metadata} = lager_test_backend:pop(), | |||||
?assertMatch(Level, lager_util:level_to_num(warning)), | |||||
?assertEqual("test message", Message), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing calling a function returns the same content on emit", | |||||
fun() -> | |||||
lager:warning("static message"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
Function = proplists:get_value(returns_static_emit, Metadata), | |||||
?assertEqual(transform_static(), Function()), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing calling a function which returns content which can change on emit", | |||||
fun() -> | |||||
lager:warning("dynamic message"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
Function = proplists:get_value(returns_dynamic_emit, Metadata), | |||||
?assert(Function() =< Function()), | |||||
?assert(Function() =< Function()), | |||||
?assert(Function() =< Function()), | |||||
?assert(Function() =< Function()), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing a undefined function returns undefined on emit", | |||||
fun() -> | |||||
lager:warning("Undefined error"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
Function = proplists:get_value(returns_undefined_emit, Metadata), | |||||
[{module, Module}, {name, Name}|_] = erlang:fun_info(Function), | |||||
?assertNot(erlang:function_exported(Module, Name, 0)), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing calling a function returns the same content on log", | |||||
fun() -> | |||||
lager:warning("static message"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
?assertEqual(transform_static(), proplists:get_value(returns_static_log, Metadata)), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing calling a dynamic function on log which returns the same value", | |||||
fun() -> | |||||
lager:warning("dynamic message"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
Value = proplists:get_value(returns_dynamic_log, Metadata), | |||||
?assert(Value =< transform_dynamic()), | |||||
?assert(Value =< transform_dynamic()), | |||||
?assert(Value =< transform_dynamic()), | |||||
?assert(Value =< transform_dynamic()), | |||||
?assert(Value =< transform_dynamic()), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing differences in results for on_log vs on emit from dynamic function", | |||||
fun() -> | |||||
lager:warning("on_log vs on emit"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
Value = proplists:get_value(returns_dynamic_log, Metadata), | |||||
Function = proplists:get_value(returns_dynamic_emit, Metadata), | |||||
FunctionResult = Function(), | |||||
?assert(Value =< FunctionResult), | |||||
?assert(Value =< Function()), | |||||
?assert(FunctionResult =< Function()), | |||||
ok | |||||
end | |||||
}, | |||||
{"Testing a function provided via metadata", | |||||
fun()-> | |||||
Provided = fun() -> | |||||
provided_metadata | |||||
end, | |||||
lager:md([{provided, Provided}]), | |||||
lager:warning("Provided metadata"), | |||||
?assertEqual(1, lager_test_backend:count()), | |||||
{_Level, _Time, _Message, Metadata} = lager_test_backend:pop(), | |||||
Function = proplists:get_value(provided, Metadata), | |||||
?assertEqual(Provided(), Function()), | |||||
ok | |||||
end | |||||
} | |||||
] | |||||
}. | |||||
-endif. |
@ -0,0 +1,103 @@ | |||||
-module(lager_trace_test). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-ifdef(TEST). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
% Our expectation is that the first log entry will appear so we won't actually | |||||
% wait out ?FIRST_LOG_ENTRY_TIMEOUT. On the other hand, the second log entry is | |||||
% expected never to arrive, so the test will wait out ?SECOND_LOG_ENTRY_TIMEOUT; | |||||
% that's why it is shorter. | |||||
-define(FIRST_LOG_ENTRY_TIMEOUT, (10 * 1000)). % 10 seconds | |||||
-define(SECOND_LOG_ENTRY_TIMEOUT, 1000). % 1 second | |||||
-define(FNAME, "test/test1.log"). | |||||
trace_test_() -> | |||||
{timeout, | |||||
10, | |||||
{foreach, | |||||
fun() -> | |||||
file:write_file(?FNAME, ""), | |||||
error_logger:tty(false), | |||||
application:load(lager), | |||||
application:set_env(lager, log_root, "test"), | |||||
application:set_env(lager, handlers, | |||||
[{lager_file_backend, | |||||
[{file, "test1.log"}, | |||||
{level, none}, | |||||
{formatter, lager_default_formatter}, | |||||
{formatter_config, [message, "\n"]} | |||||
]}]), | |||||
application:set_env(lager, traces, | |||||
[{{lager_file_backend, "test1.log"}, | |||||
[{tag, mytag}], info}]), | |||||
application:set_env(lager, error_logger_redirect, false), | |||||
application:set_env(lager, async_threshold, undefined), | |||||
lager:start() | |||||
end, | |||||
fun(_) -> | |||||
file:delete(?FNAME), | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
application:unset_env(lager, log_root), | |||||
application:unset_env(lager, handlers), | |||||
application:unset_env(lager, traces), | |||||
application:unset_env(lager, error_logger_redirect), | |||||
application:unset_env(lager, async_threshold), | |||||
error_logger:tty(true) | |||||
end, | |||||
[{"Trace combined with log_root", | |||||
fun() -> | |||||
lager:info([{tag, mytag}], "Test message"), | |||||
% Wait until we have the expected log entry in the log file. | |||||
case wait_until(fun() -> | |||||
count_lines(?FNAME) >= 1 | |||||
end, ?FIRST_LOG_ENTRY_TIMEOUT) of | |||||
ok -> | |||||
ok; | |||||
{error, timeout} -> | |||||
throw({file_empty, file:read_file(?FNAME)}) | |||||
end, | |||||
% Let's wait a little to see that we don't get a duplicate log | |||||
% entry. | |||||
case wait_until(fun() -> | |||||
count_lines(?FNAME) >= 2 | |||||
end, ?SECOND_LOG_ENTRY_TIMEOUT) of | |||||
ok -> | |||||
throw({too_many_entries, file:read_file(?FNAME)}); | |||||
{error, timeout} -> | |||||
ok | |||||
end | |||||
end} | |||||
]}}. | |||||
% Wait until Fun() returns true. | |||||
wait_until(Fun, Timeout) -> | |||||
wait_until(Fun, Timeout, {8, 13}). | |||||
wait_until(_Fun, Timeout, {T1, _}) when T1 > Timeout -> | |||||
{error, timeout}; | |||||
wait_until(Fun, Timeout, {T1, T2}) -> | |||||
case Fun() of | |||||
true -> | |||||
ok; | |||||
false -> | |||||
timer:sleep(T1), | |||||
wait_until(Fun, Timeout, {T2, T1 + T2}) | |||||
end. | |||||
% Return the number of lines in a file. Return 0 for a non-existent file. | |||||
count_lines(Filename) -> | |||||
case file:read_file(Filename) of | |||||
{ok, Content} -> | |||||
Lines = binary:split(Content, <<"\n">>, [global, trim]), | |||||
length(Lines); | |||||
{error, _} -> | |||||
0 | |||||
end. | |||||
-endif. |
@ -0,0 +1,47 @@ | |||||
-module(pr_composite_test). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-record(a, {field1 :: term(), field2 :: term()}). | |||||
-record(b, {field1 :: term() , field2 :: term()}). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
nested_record_test() -> | |||||
A = #a{field1 = x, field2 = y}, | |||||
B = #b{field1 = A, field2 = {}}, | |||||
Pr_B = lager:pr(B, ?MODULE), | |||||
?assertEqual({'$lager_record', b, | |||||
[{field1, {'$lager_record', a, | |||||
[{field1, x},{field2, y}]}}, | |||||
{field2, {}}]}, | |||||
Pr_B). | |||||
list_field_test() -> | |||||
As = [#a{field1 = 1, field2 = a2}, | |||||
#a{field1 = 2, field2 = a2}], | |||||
B = #b{field1 = As, field2 = b2}, | |||||
Pr_B = lager:pr(B, ?MODULE), | |||||
?assertEqual({'$lager_record', b, | |||||
[{field1, [{'$lager_record', a, | |||||
[{field1, 1},{field2, a2}]}, | |||||
{'$lager_record', a, | |||||
[{field1, 2},{field2, a2}]}]}, | |||||
{field2, b2}]}, | |||||
Pr_B). | |||||
list_of_records_test() -> | |||||
As = [#a{field1 = 1, field2 = a2}, | |||||
#a{field1 = 2, field2 = a2}], | |||||
Pr_As = lager:pr(As, ?MODULE), | |||||
?assertEqual([{'$lager_record', a, [{field1, 1},{field2, a2}]}, | |||||
{'$lager_record', a, [{field1, 2},{field2, a2}]}], | |||||
Pr_As). | |||||
improper_list_test() -> | |||||
A = #a{field1 = [1|2], field2 = a2}, | |||||
Pr_A = lager:pr(A, ?MODULE), | |||||
?assertEqual({'$lager_record',a, | |||||
[{field1,[1|2]},{field2,a2}]}, | |||||
Pr_A). |
@ -0,0 +1,63 @@ | |||||
-module(pr_stacktrace_test). | |||||
-compile([{parse_transform, lager_transform}]). | |||||
-ifdef(OTP_RELEASE). %% this implies 21 or higher | |||||
-define(EXCEPTION(Class, Reason, Stacktrace), Class:Reason:Stacktrace). | |||||
-define(GET_STACK(Stacktrace), Stacktrace). | |||||
-else. | |||||
-define(EXCEPTION(Class, Reason, _), Class:Reason). | |||||
-define(GET_STACK(_), erlang:get_stacktrace()). | |||||
-endif. | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
make_throw() -> | |||||
throw({test, exception}). | |||||
bad_arity() -> | |||||
lists:concat([], []). | |||||
bad_arg() -> | |||||
integer_to_list(1.0). | |||||
pr_stacktrace_throw_test() -> | |||||
Got = try | |||||
make_throw() | |||||
catch | |||||
?EXCEPTION(Class, Reason, Stacktrace) -> | |||||
lager:pr_stacktrace(?GET_STACK(Stacktrace), {Class, Reason}) | |||||
end, | |||||
Want = "pr_stacktrace_test:pr_stacktrace_throw_test/0 line 26\n pr_stacktrace_test:make_throw/0 line 16\nthrow:{test,exception}", | |||||
?assertNotEqual(nomatch, string:find(Got, Want)). | |||||
pr_stacktrace_bad_arg_test() -> | |||||
Got = try | |||||
bad_arg() | |||||
catch | |||||
?EXCEPTION(Class, Reason, Stacktrace) -> | |||||
lager:pr_stacktrace(?GET_STACK(Stacktrace), {Class, Reason}) | |||||
end, | |||||
Want = "pr_stacktrace_test:pr_stacktrace_bad_arg_test/0 line 36\n pr_stacktrace_test:bad_arg/0 line 22\nerror:badarg", | |||||
?assertNotEqual(nomatch, string:find(Got, Want)). | |||||
pr_stacktrace_bad_arity_test() -> | |||||
Got = try | |||||
bad_arity() | |||||
catch | |||||
?EXCEPTION(Class, Reason, Stacktrace) -> | |||||
lager:pr_stacktrace(?GET_STACK(Stacktrace), {Class, Reason}) | |||||
end, | |||||
Want = "pr_stacktrace_test:pr_stacktrace_bad_arity_test/0 line 46\n lists:concat([], [])\nerror:undef", | |||||
?assertNotEqual(nomatch, string:find(Got, Want)). | |||||
pr_stacktrace_no_reverse_test() -> | |||||
application:set_env(lager, reverse_pretty_stacktrace, false), | |||||
Got = try | |||||
bad_arity() | |||||
catch | |||||
?EXCEPTION(Class, Reason, Stacktrace) -> | |||||
lager:pr_stacktrace(?GET_STACK(Stacktrace), {Class, Reason}) | |||||
end, | |||||
Want = "error:undef\n lists:concat([], [])\n pr_stacktrace_test:pr_stacktrace_bad_arity_test/0 line 57", | |||||
?assertEqual(nomatch, string:find(Got, Want)). |
@ -0,0 +1,36 @@ | |||||
-module(special_process). | |||||
-export([start/0, init/1]). | |||||
start() -> | |||||
proc_lib:start_link(?MODULE, init, [self()]). | |||||
init(Parent) -> | |||||
proc_lib:init_ack(Parent, {ok, self()}), | |||||
loop(). | |||||
loop() -> | |||||
receive | |||||
function_clause -> | |||||
foo(bar), | |||||
loop(); | |||||
exit -> | |||||
exit(byebye), | |||||
loop(); | |||||
error -> | |||||
erlang:error(mybad), | |||||
loop(); | |||||
{case_clause, X} -> | |||||
case X of | |||||
notgonnamatch -> | |||||
ok; | |||||
notthiseither -> | |||||
error | |||||
end, | |||||
loop(); | |||||
_ -> | |||||
loop() | |||||
end. | |||||
foo(baz) -> | |||||
ok. | |||||
@ -0,0 +1,89 @@ | |||||
%% | |||||
%% %CopyrightBegin% | |||||
%% | |||||
%% Copyright Ericsson AB 1996-2009. All Rights Reserved. | |||||
%% | |||||
%% The contents of this file are subject to the Erlang Public License, | |||||
%% Version 1.1, (the "License"); you may not use this file except in | |||||
%% compliance with the License. You should have received a copy of the | |||||
%% Erlang Public License along with this software. If not, it can be | |||||
%% retrieved online at http://www.erlang.org/. | |||||
%% | |||||
%% Software distributed under the License is distributed on an "AS IS" | |||||
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See | |||||
%% the License for the specific language governing rights and limitations | |||||
%% under the License. | |||||
%% | |||||
%% %CopyrightEnd% | |||||
%% | |||||
-module(sync_error_logger). | |||||
%% The error_logger API, but synchronous! | |||||
%% This is helpful for tests, otherwise you need lots of nasty timer:sleep. | |||||
%% Additionally, the warning map can be set on a per-process level, for | |||||
%% convienience, via the process dictionary value `warning_map'. | |||||
-export([ | |||||
info_msg/1, info_msg/2, | |||||
warning_msg/1, warning_msg/2, | |||||
error_msg/1,error_msg/2 | |||||
]). | |||||
-export([ | |||||
info_report/1, info_report/2, | |||||
warning_report/1, warning_report/2, | |||||
error_report/1, error_report/2 | |||||
]). | |||||
info_msg(Format) -> | |||||
info_msg(Format, []). | |||||
info_msg(Format, Args) -> | |||||
gen_event:sync_notify(error_logger, {info_msg, group_leader(), {self(), Format, Args}}). | |||||
warning_msg(Format) -> | |||||
warning_msg(Format, []). | |||||
warning_msg(Format, Args) -> | |||||
gen_event:sync_notify(error_logger, {warning_msg_tag(), group_leader(), {self(), Format, Args}}). | |||||
error_msg(Format) -> | |||||
error_msg(Format, []). | |||||
error_msg(Format, Args) -> | |||||
gen_event:sync_notify(error_logger, {error, group_leader(), {self(), Format, Args}}). | |||||
info_report(Report) -> | |||||
info_report(std_info, Report). | |||||
info_report(Type, Report) -> | |||||
gen_event:sync_notify(error_logger, {info_report, group_leader(), {self(), Type, Report}}). | |||||
warning_report(Report) -> | |||||
warning_report(std_warning, Report). | |||||
warning_report(Type, Report) -> | |||||
{Tag, NType} = warning_report_tag(Type), | |||||
gen_event:sync_notify(error_logger, {Tag, group_leader(), {self(), NType, Report}}). | |||||
error_report(Report) -> | |||||
error_report(std_error, Report). | |||||
error_report(Type, Report) -> | |||||
gen_event:sync_notify(error_logger, {error_report, group_leader(), {self(), Type, Report}}). | |||||
warning_msg_tag() -> | |||||
case get(warning_map) of | |||||
warning -> warning_msg; | |||||
info -> info_msg; | |||||
_ -> error | |||||
end. | |||||
warning_report_tag(Type) -> | |||||
case {get(warning_map), Type == std_warning} of | |||||
{warning, _} -> {warning_report, Type}; | |||||
{info, true} -> {info_report, std_info}; | |||||
{info, false} -> {info_report, Type}; | |||||
{_, true} -> {error_report, std_error}; | |||||
{_, false} -> {error_report, Type} | |||||
end. |
@ -0,0 +1,244 @@ | |||||
%% ------------------------------------------------------------------- | |||||
%% | |||||
%% trunc_io_eqc: QuickCheck test for trunc_io:format with maxlen | |||||
%% | |||||
%% Copyright (c) 2011-2012 Basho Technologies, Inc. All Rights Reserved. | |||||
%% | |||||
%% 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. | |||||
%% | |||||
%% ------------------------------------------------------------------- | |||||
-module(trunc_io_eqc). | |||||
-ifdef(TEST). | |||||
-ifdef(EQC). | |||||
-export([test/0, test/1, check/0, prop_format/0, prop_equivalence/0]). | |||||
-include_lib("eqc/include/eqc.hrl"). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
-define(QC_OUT(P), | |||||
eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)). | |||||
%%==================================================================== | |||||
%% eunit test | |||||
%%==================================================================== | |||||
eqc_test_() -> | |||||
{timeout, 60, | |||||
{spawn, | |||||
[ | |||||
{timeout, 30, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_format()))))}, | |||||
{timeout, 30, ?_assertEqual(true, eqc:quickcheck(eqc:testing_time(14, ?QC_OUT(prop_equivalence()))))} | |||||
] | |||||
}}. | |||||
%%==================================================================== | |||||
%% Shell helpers | |||||
%%==================================================================== | |||||
test() -> | |||||
test(100). | |||||
test(N) -> | |||||
quickcheck(numtests(N, prop_format())). | |||||
check() -> | |||||
check(prop_format(), current_counterexample()). | |||||
%%==================================================================== | |||||
%% Generators | |||||
%%==================================================================== | |||||
gen_fmt_args() -> | |||||
list(oneof([gen_print_str(), | |||||
"~~", | |||||
{"~10000000.p", gen_any(5)}, | |||||
{"~w", gen_any(5)}, | |||||
{"~s", oneof([gen_print_str(), gen_atom(), gen_quoted_atom(), gen_print_bin(), gen_iolist(5)])}, | |||||
{"~1000000.P", gen_any(5), 4}, | |||||
{"~W", gen_any(5), 4}, | |||||
{"~i", gen_any(5)}, | |||||
{"~B", nat()}, | |||||
{"~b", nat()}, | |||||
{"~X", nat(), "0x"}, | |||||
{"~x", nat(), "0x"}, | |||||
{"~.10#", nat()}, | |||||
{"~.10+", nat()}, | |||||
{"~.36B", nat()}, | |||||
{"~1000000.62P", gen_any(5), 4}, | |||||
{"~c", gen_char()}, | |||||
{"~tc", gen_char()}, | |||||
{"~f", real()}, | |||||
{"~10.f", real()}, | |||||
{"~g", real()}, | |||||
{"~10.g", real()}, | |||||
{"~e", real()}, | |||||
{"~10.e", real()} | |||||
])). | |||||
%% Generates a printable string | |||||
gen_print_str() -> | |||||
?LET(Xs, list(char()), [X || X <- Xs, io_lib:printable_list([X]), X /= $~, X < 256]). | |||||
gen_print_bin() -> | |||||
?LET(Xs, gen_print_str(), list_to_binary(Xs)). | |||||
gen_any(MaxDepth) -> | |||||
oneof([largeint(), | |||||
gen_atom(), | |||||
gen_quoted_atom(), | |||||
nat(), | |||||
%real(), | |||||
binary(), | |||||
gen_bitstring(), | |||||
gen_pid(), | |||||
gen_port(), | |||||
gen_ref(), | |||||
gen_fun()] ++ | |||||
[?LAZY(list(gen_any(MaxDepth - 1))) || MaxDepth /= 0] ++ | |||||
[?LAZY(gen_tuple(gen_any(MaxDepth - 1))) || MaxDepth /= 0]). | |||||
gen_iolist(0) -> | |||||
[]; | |||||
gen_iolist(Depth) -> | |||||
list(oneof([gen_char(), gen_print_str(), gen_print_bin(), gen_iolist(Depth-1)])). | |||||
gen_atom() -> | |||||
elements([abc, def, ghi]). | |||||
gen_quoted_atom() -> | |||||
elements(['abc@bar', '@bar', '10gen']). | |||||
gen_bitstring() -> | |||||
?LET(XS, binary(), <<XS/binary, 1:7>>). | |||||
gen_tuple(Gen) -> | |||||
?LET(Xs, list(Gen), list_to_tuple(Xs)). | |||||
gen_max_len() -> %% Generate length from 3 to whatever. Needs space for ... in output | |||||
?LET(Xs, int(), 3 + abs(Xs)). | |||||
gen_pid() -> | |||||
?LAZY(spawn(fun() -> ok end)). | |||||
gen_port() -> | |||||
?LAZY(begin | |||||
Port = erlang:open_port({spawn, "true"}, []), | |||||
catch(erlang:port_close(Port)), | |||||
Port | |||||
end). | |||||
gen_ref() -> | |||||
?LAZY(make_ref()). | |||||
gen_fun() -> | |||||
?LAZY(fun() -> ok end). | |||||
gen_char() -> | |||||
oneof(lists:seq($A, $z)). | |||||
%%==================================================================== | |||||
%% Property | |||||
%%==================================================================== | |||||
%% Checks that trunc_io:format produces output less than or equal to MaxLen | |||||
prop_format() -> | |||||
?FORALL({FmtArgs, MaxLen}, {gen_fmt_args(), gen_max_len()}, | |||||
begin | |||||
%% Because trunc_io will print '...' when its running out of | |||||
%% space, even if the remaining space is less than 3, it | |||||
%% doesn't *exactly* stick to the specified limit. | |||||
%% Also, since we don't truncate terms not printed with | |||||
%% ~p/~P/~w/~W/~s, we also need to calculate the wiggle room | |||||
%% for those. Hence the fudge factor calculated below. | |||||
FudgeLen = calculate_fudge(FmtArgs, 50), | |||||
{FmtStr, Args} = build_fmt_args(FmtArgs), | |||||
try | |||||
Str = lists:flatten(lager_trunc_io:format(FmtStr, Args, MaxLen)), | |||||
?WHENFAIL(begin | |||||
io:format(user, "FmtStr: ~p\n", [FmtStr]), | |||||
io:format(user, "Args: ~p\n", [Args]), | |||||
io:format(user, "FudgeLen: ~p\n", [FudgeLen]), | |||||
io:format(user, "MaxLen: ~p\n", [MaxLen]), | |||||
io:format(user, "ActLen: ~p\n", [length(Str)]), | |||||
io:format(user, "Str: ~p\n", [Str]) | |||||
end, | |||||
%% Make sure the result is a printable list | |||||
%% and if the format string is less than the length, | |||||
%% the result string is less than the length. | |||||
conjunction([{printable, Str == "" orelse | |||||
io_lib:printable_list(Str)}, | |||||
{length, length(FmtStr) > MaxLen orelse | |||||
length(Str) =< MaxLen + FudgeLen}])) | |||||
catch | |||||
_:Err -> | |||||
io:format(user, "\nException: ~p\n", [Err]), | |||||
io:format(user, "FmtStr: ~p\n", [FmtStr]), | |||||
io:format(user, "Args: ~p\n", [Args]), | |||||
false | |||||
end | |||||
end). | |||||
%% Checks for equivalent formatting to io_lib | |||||
prop_equivalence() -> | |||||
?FORALL(FmtArgs, gen_fmt_args(), | |||||
begin | |||||
{FmtStr, Args} = build_fmt_args(FmtArgs), | |||||
Expected = lists:flatten(io_lib:format(FmtStr, Args)), | |||||
Actual = lists:flatten(lager_trunc_io:format(FmtStr, Args, 10485760)), | |||||
?WHENFAIL(begin | |||||
io:format(user, "FmtStr: ~p\n", [FmtStr]), | |||||
io:format(user, "Args: ~p\n", [Args]), | |||||
io:format(user, "Expected: ~p\n", [Expected]), | |||||
io:format(user, "Actual: ~p\n", [Actual]) | |||||
end, | |||||
Expected == Actual) | |||||
end). | |||||
%%==================================================================== | |||||
%% Internal helpers | |||||
%%==================================================================== | |||||
%% Build a tuple of {Fmt, Args} from a gen_fmt_args() return | |||||
build_fmt_args(FmtArgs) -> | |||||
F = fun({Fmt, Arg}, {FmtStr0, Args0}) -> | |||||
{FmtStr0 ++ Fmt, Args0 ++ [Arg]}; | |||||
({Fmt, Arg1, Arg2}, {FmtStr0, Args0}) -> | |||||
{FmtStr0 ++ Fmt, Args0 ++ [Arg1, Arg2]}; | |||||
(Str, {FmtStr0, Args0}) -> | |||||
{FmtStr0 ++ Str, Args0} | |||||
end, | |||||
lists:foldl(F, {"", []}, FmtArgs). | |||||
calculate_fudge([], Acc) -> | |||||
Acc; | |||||
calculate_fudge([{"~62P", _Arg, _Depth}|T], Acc) -> | |||||
calculate_fudge(T, Acc+62); | |||||
calculate_fudge([{Fmt, Arg}|T], Acc) when | |||||
Fmt == "~f"; Fmt == "~10.f"; | |||||
Fmt == "~g"; Fmt == "~10.g"; | |||||
Fmt == "~e"; Fmt == "~10.e"; | |||||
Fmt == "~x"; Fmt == "~X"; | |||||
Fmt == "~B"; Fmt == "~b"; Fmt == "~36B"; | |||||
Fmt == "~.10#"; Fmt == "~10+" -> | |||||
calculate_fudge(T, Acc + length(lists:flatten(io_lib:format(Fmt, [Arg])))); | |||||
calculate_fudge([_|T], Acc) -> | |||||
calculate_fudge(T, Acc). | |||||
-endif. % (EQC). | |||||
-endif. % (TEST). |
@ -0,0 +1,35 @@ | |||||
%% @doc This test is named zzzz_gh280_crash because it has to be run first and tests are run in | |||||
%% reverse alphabetical order. | |||||
%% | |||||
%% The problem we are attempting to detect here is when log_mf_h is installed as a handler for error_logger | |||||
%% and lager starts up to replace the current handlers with its own. This causes a start up crash because | |||||
%% OTP error logging modules do not have any notion of a lager-style log level. | |||||
-module(zzzz_gh280_crash). | |||||
-include_lib("eunit/include/eunit.hrl"). | |||||
gh280_crash_test() -> | |||||
{timeout, 30, fun() -> gh280_impl() end}. | |||||
gh280_impl() -> | |||||
application:stop(lager), | |||||
application:stop(goldrush), | |||||
error_logger:tty(false), | |||||
%% see https://github.com/erlang/otp/blob/maint/lib/stdlib/src/log_mf_h.erl#L81 | |||||
%% for an explanation of the init arguments to log_mf_h | |||||
ok = gen_event:add_sup_handler(error_logger, log_mf_h, log_mf_h:init("/tmp", 10000, 5)), | |||||
lager:start(), | |||||
Result = receive | |||||
{gen_event_EXIT,log_mf_h,normal} -> | |||||
true; | |||||
{gen_event_EXIT,Handler,Reason} -> | |||||
{Handler,Reason}; | |||||
X -> | |||||
X | |||||
after 10000 -> | |||||
timeout | |||||
end, | |||||
?assert(Result), | |||||
application:stop(lager), | |||||
application:stop(goldrush). |