-module(eRum). -include("eRum.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 , getLogLevels/1 , updateLogevelCfg/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 ]). -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 }). %% API %% @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). %% @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(_App1, {error, {already_started, _App2}}) -> 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(), rumAtomLevel(), 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 = rumUtil:levelToNum(Severity), case {whereis(Sink), whereis(?RumDefSink), rumConfig: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}; _ -> {rumUtil: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 = rumMsg:new(Msg, Severity, Metadata, Destinations), case rumConfig:get({Sink, async}, false) of true -> gen_event:notify(SinkPid, {mWriteLog, LagerMsg}); false -> gen_event:sync_notify(SinkPid, {mWriteLog, LagerMsg}) end, case TraceSinkPid /= undefined of true -> gen_event:notify(TraceSinkPid, {mWriteLog, 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(?RumDefSink, 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, ?RumDefSink, 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(rumAtomLevel(), 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, [], ?RumDefTruncation); log(Level, Metadata, Message) when is_list(Metadata) -> dispatch_log(Level, Metadata, Message, [], ?RumDefTruncation). %% @doc Manually log a message into lager without using the parse transform. -spec log(rumAtomLevel(), 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, ?RumDefTruncation); log(Level, Metadata, Format, Args) when is_list(Metadata) -> dispatch_log(Level, Metadata, Format, Args, ?RumDefTruncation). log_unsafe(Level, Metadata, Format, Args) when is_list(Metadata) -> dispatch_log(?RumDefSink, Level, Metadata, Format, Args, ?RumDefTruncation, unsafe). %% @doc Manually log a message into lager without using the parse transform. -spec log(atom(), rumAtomLevel(), 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, ?RumDefTruncation, safe); log(Sink, Level, Metadata, Format, Args) when is_list(Metadata) -> dispatch_log(Sink, Level, Metadata, Format, Args, ?RumDefTruncation, safe). validate_trace_filters(Filters, Level, Backend) -> Sink = proplists:get_value(sink, Filters, ?RumDefSink), {Sink, rumUtil: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 = rumUtil:parsePath(File), case validate_trace_filters(Filter, Level, {lager_file_backend, FileName}) of {Sink, {ok, Trace}} -> Handlers = rumConfig:global_get(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, {lager_file_backend, FileName}, LogFileConfig), rumConfig: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} = rumConfig:get({Sink, loglevel}), NewTraces = lists:delete(Trace, Traces), _ = rumUtil:trace_filter([element(1, T) || T <- NewTraces]), %MinLevel = minimum_loglevel(get_loglevels() ++ get_trace_levels(NewTraces)), rumConfig: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, []), rumConfig:global_set(handlers, lists:keydelete(Backend, 1, rumConfig: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(), rumConfig:global_get(handlers, []))). clear_traces_by_sink(Sinks) -> lists:foreach(fun(S) -> {Level, _Traces} = rumConfig:get({S, loglevel}), rumConfig: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 = rumConfig:global_get(handlers, []), clear_traces_by_sink(list_all_sinks()), _ = rumUtil:trace_filter(none), rumConfig: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} = rumConfig:get({S, loglevel}), Acc ++ lists:map(fun(T) -> {S, T} end, Traces) end, [], Sinks). status() -> Handlers = rumConfig: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 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 {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(?RumDefSink, 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(?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.) 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, {mSetLogLevel, Level}, infinity), updateLogevelCfg(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(?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. get_loglevel(Sink, Handler) -> case gen_event:call(Sink, Handler, mGetLogLevel, infinity) of Mask when is_integer(Mask) -> case rumUtil:maskToLevels(Mask) of [] -> none; Levels -> hd(Levels) end; 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], ?RumDefTruncation). getLogLevels(Sink) -> [gen_event:call(Sink, Handler, mGetLogLevel, 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(?RumDefSink, Handler, Hwm). %% @doc Set the loghwm for a particular backend. set_loghwm(Sink, Handler, Hwm) when is_integer(Hwm) -> gen_event:call(Sink, Handler, {mSetLogHwm, 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}, {mSetLogHwm, Hwm}, infinity). %% @private add_trace_to_loglevel_config(Trace, Sink) -> {MinLevel, Traces} = rumConfig:get({Sink, loglevel}), case lists:member(Trace, Traces) of false -> NewTraces = [Trace | Traces], _ = rumUtil:trace_filter([element(1, T) || T <- NewTraces]), rumConfig:set({Sink, loglevel}, {MinLevel, [Trace | Traces]}); _ -> ok end. %% @doc recalculate min log level updateLogevelCfg(error_logger) -> %% Not a sink under our control, part of the Erlang logging %% utility that error_logger_lager_h attaches to true; updateLogevelCfg(Sink) -> {_, Traces} = rumConfig:get({Sink, loglevel}, {ignore_me, []}), MinLog = minLogLevel(getLogLevels(Sink)), rumConfig:set({Sink, loglevel}, {MinLog, Traces}). minLogLevel(Levels) -> lists:foldl( fun(Mask, Acc) -> Mask bor 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 rumTruncIo:format(Fmt, Args, Limit, Options) catch _:_ -> rumTruncIo: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 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. 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 ++ rumErrLoggerH: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 = rumConfig: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, rumConfig:global_get(handlers))). rotate_handlers(Handlers) -> [rotate_handler(Handler, Sink) || {Handler, Sink} <- Handlers]. rotate_handler(Handler) -> Handlers = rumConfig: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, mRotate, ?ROTATE_TIMEOUT). %% @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. -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(?RumDefSink, Handler, debug), ?assertNotEqual(nomatch, string:find(Status, File)). -endif.