-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.