%% 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'). %% API -export([start/0, log/3, log/4, log/5, md/0, md/1, trace/2, trace/3, trace_file/2, trace_file/3, trace_file/4, trace_console/1, trace_console/2, clear_all_traces/0, stop_trace/1, stop_trace/3, status/0, get_loglevel/1, set_loglevel/2, set_loglevel/3, set_loglevel/4, get_loglevels/1, update_loglevel_config/1, posix_error/1, safe_format/3, safe_format_chop/3, dispatch_log/5, dispatch_log/6, dispatch_log/9, do_log/10, pr/2]). -type log_level() :: debug | info | notice | warning | error | critical | alert | emergency. -type log_level_number() :: 0..7. -export_type([log_level/0, log_level_number/0]). %% API %% @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). dispatch_log(Severity, Metadata, Format, Args, Size) when is_atom(Severity)-> dispatch_log(?DEFAULT_SINK, Severity, Metadata, Format, Args, Size). -spec dispatch_log(atom(), log_level(), list(), string(), list() | none, pos_integer()) -> ok | {error, lager_not_running}. %% this is the same check that the parse transform bakes into the module at compile time dispatch_log(Sink, Severity, Metadata, Format, Args, Size) when is_atom(Severity)-> SeverityAsInt=lager_util:level_to_num(Severity), case {whereis(Sink), lager_config:get({Sink, loglevel}, {?LOG_NONE, []})} of {undefined, _} -> {error, lager_not_running}; {SinkPid, {Level, Traces}} when (Level band SeverityAsInt) /= 0 orelse Traces /= [] -> do_log(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) -> {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) -> safe_format_chop(Format,Args,Size); _ -> Format end, LagerMsg = lager_msg:new(Msg, Severity, Metadata, Destinations), case lager_config:get({Sink, async}, false) of %% this needs to be able to get value from a non-default sink 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. %% 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). %% 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). %% @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); log(Sink, Level, Metadata, Format, Args) when is_list(Metadata) -> dispatch_log(Sink, Level, Metadata, Format, Args, ?DEFAULT_TRUNCATION). 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) -> case validate_trace_filters(Filter, Level, {lager_file_backend, File}) of {Sink, {ok, Trace}} -> Handlers = lager_config:global_get(handlers, []), %% check if this file backend is already installed Res = case lists:keyfind({lager_file_backend, File}, 1, Handlers) of false -> %% install the handler LogFileConfig = lists:keystore(level, 1, lists:keystore(file, 1, Options, {file, File}), {level, none}), HandlerInfo = lager_app:start_handler(Sink, lager_file_backend, 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, _} -> %% XXX Double-check this logic for {ok, exists} add_trace_to_loglevel_config(Trace, Sink), {ok, {{lager_file_backend, File}, 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). stop_trace_int({Backend, _Filter, _Level} = 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, []); _ -> ok end; _ -> ok end, ok. clear_all_traces() -> Handlers = lager_config:global_get(handlers, []), _ = lager_util:trace_filter(none), lists:foreach(fun({_Watcher, Handler, Sink}) -> case get_loglevel(Sink, Handler) of none -> gen_event:delete_handler(Sink, Handler, []); _ -> ok end end, Handlers). %% XXX Needs heavy revision status() -> Handlers = lager_config:global_get(handlers, []), TraceCount = case length(element(2, lager_config:get(loglevel))) of 0 -> 1; N -> N end, Status = ["Lager status:\n", [begin Level = get_loglevel(Handler), case Handler of {lager_file_backend, File} -> io_lib:format("File ~s at level ~p\n", [File, Level]); lager_console_backend -> io_lib:format("Console at level ~p\n", [Level]); _ -> [] end end || Handler <- 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 at level ~p to ~p\n", [Filter, LevelName, Destination]) end || {Filter, Level, Destination} <- element(2, lager_config:get(loglevel))], [ "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). %% @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)]. %% @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(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}]). %% @doc Print a record lager found during parse transform pr(Record, Module) when is_tuple(Record), is_atom(element(1, Record)) -> try case is_record_known(Record, Module) of false -> Record; {RecordName, RecordFields} -> {'$lager_record', RecordName, zip(RecordFields, tl(tuple_to_list(Record)), Module, [])} end catch error:undef -> Record end; pr(Record, _) -> Record. zip([FieldName|RecordFields], [FieldValue|Record], Module, ToReturn) -> case is_tuple(FieldValue) andalso tuple_size(FieldValue) > 0 andalso is_atom(element(1, FieldValue)) andalso is_record_known(FieldValue, Module) of false -> zip(RecordFields, Record, Module, [{FieldName, FieldValue}|ToReturn]); _Else -> F = {FieldName, pr(FieldValue, Module)}, zip(RecordFields, Record, Module, [F|ToReturn]) end; zip([], [], _Module, 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.