-module(eRum). -include("rumDef.hrl"). -include("rumCom.hrl"). -compile(inline). -compile({inline_size, 150}). %% API -export([ %% start stop start/0 , stop/0 %% log and log param , dispatchLog/12 , doLogImpl/13 , safeFormat/3 , unsafeFormat/2 , getMd/0 , setMd/1 , getLogLevel/1 , getLogLevel/2 , setLogLevel/2 , setLogLevel/3 , setLogLevel/4 , getLogLevels/1 , upLogLevelCfg/1 , setLogHwm/2 , setLogHwm/3 , setLogHwm/4 , rotateHandler/1 , rotateHandler/2 , rotateSink/1 , rotateAll/0 %% stack parse , parseStack/1 , parseStack/3 %% trace , trace/2 , trace/3 , traceFile/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 ]). -record(trace_func_state_v1, { pid :: undefined | pid(), level :: rumAtomLevel(), count :: infinity | pos_integer(), format_string :: string(), timeout :: infinity | pos_integer(), started = os:timestamp() :: erlang:timestamp() %% use os:timestamp for compatability }). start() -> application:ensure_all_started(eRum). stop() -> application:stop(eRum). -spec dispatchLog(atom(), rumAtomLevel(), pid(), node(), atom(), atom(), integer(), 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 rumTransform (lines 173-216) dispatchLog(Sink, Severity, Pid, Node, Module, Function, Line, Metadata, Format, Args, Size, Safety) -> case ?eRumCfg:get(Sink) band Severity /= 0 of true -> doLogImpl(Severity, Pid, Node, Module, Function, Line, Metadata, Format, Args, Severity, Size, Sink, Safety); _ -> ok end. doLogImpl(Severity, Pid, Node, Module, Function, Line, Metadata, Format, Args, Severity, Size, Sink, Safety) -> TraceFilters = rumConfig:ptGet({Sink, trace}, []), Destinations = ?IIF(TraceFilters /= [], rumUtil:check_traces(Metadata, Severity, TraceFilters, []), []), MsgStr = ?IIF(Args /= [] andalso Args /= undefined, ?IIF(Safety == safe, safeFormat(Format, [Args], [{charsLimit, Size}]), unsafeFormat(Format, [Args])), Format), NowMs = rumTime:nowMs(), NowStr = rumUtil:msToBinStr(NowMs), RumMsg = #rumMsg{severity = Severity, pid = Pid, node = Node, module = Module, function = Function, line = Line, metadata = Metadata, datetime = NowStr, timestamp = NowMs, message = MsgStr, destinations = Destinations}, case rumConfig:ptGet({Sink, async}, false) of true -> gen_emm:info_notify(Sink, {mWriteLog, RumMsg}); false -> gen_emm:call_notify(Sink, {mWriteLog, RumMsg}) end, case whereis(?RumTrackSink) of undefined -> ok; TraceSinkPid -> gen_emm:info_notify(TraceSinkPid, {mWriteLog, RumMsg}) end. %% @doc Get lager metadata for current process -spec getMd() -> [{atom(), any()}]. getMd() -> case erlang:get(?PdMdKey) 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 setMd([{atom(), any()}, ...]) -> ok. setMd(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(?PdMdKey, NewMD), ok; _ -> erlang:error(badarg) end; setMd(_) -> erlang:error(badarg). %% @doc Set the loglevel for a particular backend. setLogLevel(Handler, Level) when is_atom(Level) -> setLogLevel(?RumDefSink, Handler, undefined, Level). %% @doc Set the loglevel for a particular backend that has multiple identifiers %% (eg. the file backend). setLogLevel(Handler, Ident, Level) when is_atom(Level) -> setLogLevel(?RumDefSink, 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.) setLogLevel(Sink, Handler, Ident, Level) when is_atom(Level) -> HandlerArg = case Ident of undefined -> Handler; _ -> {Handler, Ident} end, Reply = gen_emm:call(Sink, HandlerArg, {mSetLogLevel, Level}, infinity), upLogLevelCfg(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. getLogLevel(Handler) -> getLogLevel(?RumDefSink, Handler). %% @doc Get the loglevel for a particular sink's backend. In the case that the backend %% has multiple identifiers, the lowest is returned. getLogLevel(Sink, Handler) -> case gen_emm:call(Sink, Handler, mGetLogLevel, infinity) of Mask when is_integer(Mask) -> case rumUtil:maskToLevels(Mask) of [] -> none; Levels -> hd(Levels) end; Y -> Y end. getLogLevels(Sink) -> [gen_emm:call(Sink, Handler, mGetLogLevel, infinity) || Handler <- gen_emm:which_epm(Sink)]. %% @doc Set the loghwm for the default sink. setLogHwm(Handler, Hwm) when is_integer(Hwm) -> setLogHwm(?RumDefSink, Handler, Hwm). %% @doc Set the loghwm for a particular backend. setLogHwm(Sink, Handler, Hwm) when is_integer(Hwm) -> gen_emm:call(Sink, Handler, {mSetLogHwm, Hwm}, infinity). %% @doc Set the loghwm (log high water mark) for file backends with multiple identifiers setLogHwm(Sink, Handler, Ident, Hwm) when is_integer(Hwm) -> gen_emm:call(Sink, {Handler, Ident}, {mSetLogHwm, Hwm}, infinity). %% @doc recalculate min log level upLogLevelCfg(error_logger) -> %% Not a sink under our control, part of the Erlang logging %% utility that error_logger_lager_h attaches to true; upLogLevelCfg(Sink) -> Traces = rumConfig:ptGet({Sink, trace}, []), AllLogLevel = allLogLevel(getLogLevels(Sink), 0), case Traces /= [] of true -> ets:insert(?eRumEts, {Sink, 16#ff}), AllSinks = ets:tab2list(?eRumEts), rumKvsToBeam:load(?eRumCfg, AllSinks); _ -> ets:insert(?eRumEts, {Sink, AllLogLevel}), AllSinks = ets:tab2list(?eRumEts), rumKvsToBeam:load(?eRumCfg, AllSinks) end. allLogLevel([], Acc) -> Acc; allLogLevel([OneLv | Levels], Acc) -> allLogLevel(Levels, OneLv bor Acc). rotateSink(Sink) -> Handlers = rumConfig:ptGet(handlers, []), RotateHandlers = lists:filtermap( fun({Handler, _, S}) when S == Sink -> {true, {Handler, Sink}}; (_) -> false end, Handlers), rotateHandlers(RotateHandlers). rotateAll() -> rotateHandlers(lists:map(fun({H, _, S}) -> {H, S} end, rumConfig:ptGet(handlers, []))). rotateHandlers(Handlers) -> [rotateHandler(Handler, Sink) || {Handler, Sink} <- Handlers]. rotateHandler(Handler) -> Handlers = rumConfig:ptGet(handlers, []), case lists:keyfind(Handler, 1, Handlers) of {Handler, _, Sink} -> rotateHandler(Handler, Sink); false -> ok end. rotateHandler(Handler, Sink) -> gen_emm:call(Sink, Handler, mRotate, ?RumRotateTimeout). %% @doc Print stacktrace in human readable form parseStack(Stacktrace) -> << begin case Location of [] -> <<" ", (atom_to_binary(Mod, utf8))/binary, ":", (atom_to_binary(Func, utf8))/binary, "(", (eFmt:formatBin("~w", [Arity]))/binary, ")\n">>; [{file, File}, {line, Line}] -> <<" ", (atom_to_binary(Mod, utf8))/binary, ":", (atom_to_binary(Func, utf8))/binary, "/", (integer_to_binary(Arity))/binary, "(", (unicode:characters_to_binary(File))/binary, ":", (integer_to_binary(Line))/binary, ")\n">>; _ -> <<" ", (atom_to_binary(Mod, utf8))/binary, ":", (atom_to_binary(Func, utf8))/binary, "(", (eFmt:formatBin("~w", [Arity]))/binary, ")", (eFmt:formatBin("~w", [Location]))/binary, "\n">> end end || {Mod, Func, Arity, Location} <- Stacktrace >>. parseStack(Class, Reason, Stacktrace) -> eFmt:formatBin(<<"~n Class:~s~n Reason:~p~n Stacktrace:~s">>, [Class, Reason, parseStack(Stacktrace)]). trace(BkdMod, Filter) -> trace(BkdMod, Filter, debug). trace({rumBkdFile, File}, Filter, Level) -> trace_file(File, Filter, Level); trace(Backend, Filter, Level) -> case validateTraceFilters(Filter, Level, Backend) of {Sink, {ok, Trace}} -> add_trace_to_loglevel_config(Trace, Sink), {ok, {Backend, Filter, Level}}; {_Sink, Error} -> Error end. traceFile(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 = rumUtil:parsePath(File), case validateTraceFilters(Filter, Level, {rumBkdFile, FileName}) of {Sink, {ok, Trace}} -> Handlers = rumConfig:ptGet(handlers, []), %% check if this file backend is already installed Res = case rumUtil:find_file(FileName, Handlers) of false -> %% install the handler LogFileConfig = lists:keystore(level, 1, lists:keystore(file, 1, Options, {file, FileName}), {level, none}), HandlerInfo = eRum_app:startHandler(Sink, {rumBkdFile, FileName}, LogFileConfig), rumConfig:ptSet(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, {{rumBkdFile, FileName}, Filter, Level}}; {error, _} = E -> E end; {_Sink, Error} -> Error end. trace_console(Filter) -> trace_console(Filter, debug). trace_console(Filter, Level) -> trace(rumBkdConsole, Filter, Level). stop_trace(Backend, Filter, Level) -> case validateTraceFilters(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). validateTraceFilters(Filters, Level, Backend) -> Sink = proplists:get_value(sink, Filters, ?RumDefSink), {Sink, rumUtil:validate_trace({ proplists:delete(sink, Filters), Level, Backend }) }. %% 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) -> Traces = rumConfig:ptGet({Sink, trace}, []), NewTraces = lists:delete(Trace, Traces), _ = rumUtil:trace_filter([element(1, T) || T <- NewTraces]), %MinLevel = minimum_loglevel(get_loglevels() ++ get_trace_levels(NewTraces)), rumConfig:ptSet({Sink, trace}, NewTraces), eRum:upLogLevelCfg(Sink), case getLogLevel(Sink, Backend) of none -> %% check no other traces point here case lists:keyfind(Backend, 3, NewTraces) of false -> gen_emm:delEpm(Sink, Backend, []), rumConfig:ptSet(handlers, lists:keydelete(Backend, 1, rumConfig:ptGet(handlers, []))); _ -> ok end; _ -> ok end, ok. %% @doc installs a lager trace handler into the target process (using sys:install) at the specified level. -spec install_trace(pid(), rumAtomLevel()) -> ok. install_trace(Pid, Level) -> install_trace(Pid, Level, []). -spec install_trace(pid(), rumAtomLevel(), [{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). list_all_sinks() -> sets:to_list( lists:foldl(fun({_Watcher, _Handler, Sink}, Set) -> sets:add_element(Sink, Set) end, sets:new(), rumConfig:ptGet(handlers, []))). clear_traces_by_sink(Sinks) -> lists:foreach( fun(S) -> rumConfig:ptSet({S, trace}, []), eRum:upLogLevelCfg(S) 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 = rumConfig:ptGet(handlers, []), clear_traces_by_sink(list_all_sinks()), _ = rumUtil:trace_filter(none), rumConfig:ptSet(handlers, lists:filter( fun({Handler, _Watcher, Sink}) -> case getLogLevel(Sink, Handler) of none -> gen_emm:delEpm(Sink, Handler, []), false; _ -> true end end, Handlers)). find_traces(Sinks) -> lists:foldl(fun(S, Acc) -> Traces = rumConfig:ptGet({S, trace}, []), Acc ++ lists:map(fun(T) -> {S, T} end, Traces) end, [], Sinks). status() -> Handlers = rumConfig:ptGet(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 = getLogLevel(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 rumUtil:maskToLevels(Level) of [] -> none; Levels -> hd(Levels) 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 ?RumDefTracer:info('query') of {null, false} -> ""; Query -> io_lib:format("~p~n", [Query]) end ], [ "Tracing Statistics:\n ", [begin [" ", atom_to_list(Table), ": ", integer_to_list(?RumDefTracer:info(Table) div TraceCount), "\n"] end || Table <- [input, output, filter]] ]], io:put_chars(Status). get_sink_handler_status(Sink, Handler, Level) -> case Handler of {rumBkdFile, File} -> io_lib:format("File ~ts (~s) at level ~p\n", [File, Sink, Level]); rumBkdConsole -> io_lib:format("Console (~s) at level ~p\n", [Sink, Level]); _ -> [] end. %% @private add_trace_to_loglevel_config(Trace, Sink) -> Traces = rumConfig:ptGet({Sink, trace}, []), case lists:member(Trace, Traces) of false -> NewTraces = [Trace | Traces], _ = rumUtil:trace_filter([element(1, T) || T <- NewTraces]), rumConfig:ptSet({Sink, trace}, [Trace | Traces]), eRum:upLogLevelCfg(Sink); _ -> ok end. %% @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. unsafeFormat(Fmt, Args) -> try io_lib:format(Fmt, Args) catch _:_ -> io_lib:format("FORMAT ERROR: ~p ~p", [Fmt, Args]) end. safeFormat(Fmt, Args, Limit) -> try eFmt:formatBin(Fmt, Args, [{charsLimit, Limit}]) catch _:_ -> eFmt:formatBin(<<"FORMAT ERROR: ~p ~p">>, [Fmt, Args], [{charsLimit, Limit}]) end. %% @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 only 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. %% @private trace_func(#trace_func_state_v1{pid = Pid, level = Level, format_string = Fmt} = FuncState, Event, ProcState) -> _ = eRum: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.