rewrite from lager
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

517 lines
20 KiB

-module(rumBkdConsole).
%% Configuration is a proplist with the following keys:
%%`level' - log level to use
%%`use_stderr' - either `true' or `false', defaults to false. If set to true, use standard error to output console log messages
%%`formatter' - the module to use when formatting log messages. Defaults to `rumFormatter'
%%`formatter_config' - the format configuration string. Defaults to `time [ severity ] message'
-behaviour(gen_emm).
-include("eRum.hrl").
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-compile([{parse_transform, rumTransform}]).
-endif.
-define(TERSE_FORMAT,[time, " ", color, "[", severity,"] ", message]).
-define(DEFAULT_FORMAT_CONFIG, ?TERSE_FORMAT ++ [eol()]).
-define(FORMAT_CONFIG_OFF, [{eol, eol()}]).
-define(DefOpts, [{use_stderr, false}, {group_leader, false}, {id, ?MODULE}, {formatter, rumFormatter}, {formatter_config, ?DEFAULT_FORMAT_CONFIG}]).
-export([
init/1
, handleCall/2
, handleEvent/2
, handleInfo/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()
}).
init(Opts) ->
case isNewStyleConsole() 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 = checkOpts(Opts),
Colors = ?IIF(rumUtil:get_env(colored, false), rumUtil:get_env(colors, []), []),
Level = rumUtil:get_opt(level, Opts, undefined),
L = rumUtil:config_to_mask(Level),
[UseErr, GroupLeader, ID, Formatter, Config] = [rumUtil:get_opt(Key, Opts, Def) || {Key, Def} <- ?DefOpts],
Out = ?IIF(UseErr, standard_error, ?IIF(GroupLeader == false, user, begin erlang:monitor(process, GroupLeader), GroupLeader end)),
{ok, #state{level = L, id = ID, out = Out, formatter = Formatter, format_config = Config, colors = Colors}}
end.
checkOpts([]) -> true;
checkOpts([{level, L} | T]) when is_atom(L) ->
?IIF(lists:member(L, ?RumLevels) == false, throw({error, {fatal, {bad_level, L}}}), checkOpts(T));
checkOpts([{use_stderr, true} | T]) ->
checkOpts(T);
checkOpts([{use_stderr, false} | T]) ->
checkOpts(T);
checkOpts([{formatter, M} | T]) when is_atom(M) ->
checkOpts(T);
checkOpts([{formatter_config, C} | T]) when is_list(C) ->
checkOpts(T);
checkOpts([{group_leader, L} | T]) when is_pid(L) ->
checkOpts(T);
checkOpts([{id, {?MODULE, _}} | T]) ->
checkOpts(T);
checkOpts([H | _]) ->
throw({error, {fatal, {bad_console_config, H}}}).
handleCall(mGetLogLevel, #state{level = Level} = State) ->
{ok, Level, State};
handleCall({mSetLogLevel, Level}, State) ->
try rumUtil:config_to_mask(Level) of
Levels ->
{ok, ok, State#state{level = Levels}}
catch
_:_ ->
{ok, {error, bad_log_level}, State}
end;
handleCall(_Request, State) ->
{ok, ok, State}.
%% @private
handleEvent({mWriteLog, Message},
#state{level = L, out = Out, formatter = Formatter, format_config = FormatConfig, colors = Colors, id = ID} = State) ->
case rumUtil:is_loggable(Message, L, ID) of
true ->
io:put_chars(Out, Formatter:format(Message, FormatConfig, Colors)),
{ok, State};
false ->
{ok, State}
end;
handleEvent(_Event, State) ->
{ok, State}.
handleInfo({'DOWN', _, process, Out, _}, #state{out = Out}) ->
remove_handler;
handleInfo(_Info, State) ->
{ok, State}.
terminate(remove_handler, _State = #state{id = ID}) ->
%% have to do this asynchronously because we're in the event handlr
spawn(fun() -> eRum:clear_trace_by_destination(ID) end),
ok;
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
eol() ->
case application:get_env(lager, colored) of
{ok, true} ->
"\e[0m\r\n";
_ ->
"\r\n"
end.
isNewStyleConsole() ->
%% 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)).
-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, checkOpts(Good)),
?_assertThrow({error, {fatal, {bad_level, foo}}}, checkOpts(Bad1)),
?_assertThrow({error, {fatal, {bad_console_config, {use_stderr, flase}}}}, checkOpts(Bad2)),
?_assertEqual(true, checkOpts(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, errLoggerRedirect, false),
eRum: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(rumEvent)),
gen_event:add_handler(rumEvent, lager_console_backend, [{level, info}]),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
eRum: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(rumEvent)),
gen_event:add_handler(rumEvent, lager_console_backend, [info, true]),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
eRum: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(rumEvent)),
gen_event:add_handler(rumEvent, lager_console_backend,
[{level, info}, {formatter, lager_default_formatter}, {formatter_config, [date, "#", time, "#", severity, "#", node, "#", pid, "#",
module, "#", function, "#", file, "#", line, "#", message, "\r\n"]}]),
rumConfig:set({rumEvent, loglevel}, {?INFO, []}),
eRum: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(rumEvent, lager_console_backend, [{level, info}]),
erlang:group_leader(Pid, whereis(rumEvent)),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
eRum: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, _} = eRum:trace_console([{module, ?MODULE}]),
eRum: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(rumEvent, lager_console_backend, [{level, info}]),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
erlang:group_leader(Pid, whereis(rumEvent)),
eRum: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, _} = eRum:trace_console([{module, ?MODULE}]),
eRum: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(rumEvent, lager_console_backend, [{level, info}]),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
eRum:set_loglevel(lager_console_backend, '!=info'),
erlang:group_leader(Pid, whereis(rumEvent)),
eRum: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
eRum: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(rumEvent, lager_console_backend, [{level, info}]),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
eRum:set_loglevel(lager_console_backend, '=debug'),
erlang:group_leader(Pid, whereis(rumEvent)),
eRum: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
eRum: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(rumEvent, lager_console_backend, [{level, info}, {group_leader, Pid}]),
rumConfig:set({rumEvent, loglevel}, {element(2, rumUtil:config_to_mask(info)), []}),
eRum:info("Test message"),
?assertNotEqual({group_leader, Pid}, erlang:process_info(whereis(rumEvent), 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(rumEvent))),
eRum: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 = rumConfig:global_get(handlers, []),
HandlerInfo = lager_app:start_handler(rumEvent, ID,
[{level, none}, {group_leader, Pid},
{id, ID}]),
rumConfig:global_set(handlers, [HandlerInfo | Handlers]),
eRum:info("Test message"),
?assertNotEqual({group_leader, Pid}, erlang:process_info(whereis(rumEvent), group_leader)),
receive
{io_request, From, ReplyAs, {put_chars, unicode, _Msg}} ->
From ! {io_reply, ReplyAs, ok},
?assert(false)
after
500 ->
?assert(true)
end,
eRum:trace(ID, [{module, ?MODULE}], debug),
eRum: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, []}, rumConfig:get({rumEvent, 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(rumEvent))),
%% finally, check the trace has been removed
?assertEqual({0, []}, rumConfig:get({rumEvent, loglevel})),
eRum: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, errLoggerRedirect, false),
eRum:start()
end,
fun(_) ->
application:stop(lager),
application:stop(goldrush),
error_logger:tty(true)
end,
[
{"Get/set loglevel test",
fun() ->
?assertEqual(info, eRum:get_loglevel(lager_console_backend)),
eRum:set_loglevel(lager_console_backend, debug),
?assertEqual(debug, eRum:get_loglevel(lager_console_backend)),
eRum:set_loglevel(lager_console_backend, '!=debug'),
?assertEqual(info, eRum:get_loglevel(lager_console_backend)),
eRum:set_loglevel(lager_console_backend, '!=info'),
?assertEqual(debug, eRum:get_loglevel(lager_console_backend)),
ok
end
},
{"Get/set invalid loglevel test",
fun() ->
?assertEqual(info, eRum:get_loglevel(lager_console_backend)),
?assertEqual({error, bad_log_level},
eRum:set_loglevel(lager_console_backend, fatfinger)),
?assertEqual(info, eRum:get_loglevel(lager_console_backend))
end
}
]
}.
-endif.