%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Copyright 2019 by Fred Dushin % % % % This program is free software; you can redistribute it and/or modify % % it under the terms of the GNU Lesser General Public License as % % published by the Free Software Foundation; either version 2 of the % % License, or (at your option) any later version. % % % % This program is distributed in the hope that it will be useful, % % but WITHOUT ANY WARRANTY; without even the implied warranty of % % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the % % GNU General Public License for more details. % % % % You should have received a copy of the GNU General Public License % % along with this program; if not, write to the % % Free Software Foundation, Inc., % % 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%----------------------------------------------------------------------------- %% @doc A simple logging module. %% %% This module can be used to log messages to the console. %% %% Most applications should use the logging macros defined in logger.hrl %% to do logging. %% @end %%----------------------------------------------------------------------------- -module(logger). -export([ start/0, start/1, log/3, get_levels/0, set_levels/1, get_filter/0, set_filter/1, get_sinks/0, set_sinks/1, stop/0 ]). -export([loop/1, console_log/1]). -record(state, { pid, config }). -type location() :: { Module::module(), Function::atom(), Arity::non_neg_integer(), Line::non_neg_integer() }. -type level() :: debug | info | warning | error. -type sink() :: {atom(), atom()}. -type year() :: integer(). -type month() :: 1..12. -type day() :: 1..31. -type hour() :: 0..59. -type minute() :: 0..59. -type second() :: 0..59. -type timestamp() :: {year(), month(), day(), hour(), minute(), second()}. -type msg_format() :: {string(), list(term())}. -type log_request() :: {location(), timestamp(), pid(), level(), msg_format()}. -type config_item() :: {levels, [level()]}. -type config() :: [config_item()]. -define(DEFAULT_CONFIG, [{levels, [info, warning, error]}, {filter, []}, {sinks, [{?MODULE, console_log}]}]). %%----------------------------------------------------------------------------- %% @equiv start([{levels, [info, warning, error]}]) %% @doc Start the logger. %% @end %%----------------------------------------------------------------------------- -spec start() -> {ok, pid()}. start() -> start([]). %%----------------------------------------------------------------------------- %% @param Config the logging configuration %% @returns {ok, Pid} %% @doc Start the logger. %% %% This function will start the logger with the specified configuration. %% @end %%----------------------------------------------------------------------------- -spec start(Config::config()) -> {ok, pid()}. start(Config0) -> Config = fill_defaults(Config0, ?DEFAULT_CONFIG), LoggerPid = case whereis(?MODULE) of undefined -> State = #state{pid=self(), config=Config}, Pid = spawn(?MODULE, loop, [State]), receive started -> ok end, erlang:register(?MODULE, Pid), Pid; Pid -> Pid end, {ok, LoggerPid}. %%----------------------------------------------------------------------------- %% @doc Stop the logger. %% %% This function is rarely used. %% @end %%----------------------------------------------------------------------------- -spec stop() -> ok. stop() -> {ok, Pid} = maybe_start(whereis(?MODULE)), Pid ! stop, ok. %%----------------------------------------------------------------------------- %% @param Location the location in the source module %% @doc Log a message the the specified location with the specified level. %% %% Users should use the logging macros in logger.hrl instead of %% calling this function directly. %% @end %%----------------------------------------------------------------------------- -spec log(Location::location(), Level::level(), MsgFormat::{Format::string(), Args::list(term())}) -> ok. log(Location, Level, MsgFormat) -> {ok, Pid} = maybe_start(whereis(?MODULE)), Pid ! {Location, erlang:universaltime(), self(), Level, MsgFormat}, ok. %%----------------------------------------------------------------------------- %% @param Levels the list of levels to set %% @return ok %% @doc Set the levels in the logger. %% @end %%----------------------------------------------------------------------------- -spec set_levels(Levels::[level()]) -> ok. set_levels(Levels) -> {ok, Pid} = maybe_start(whereis(?MODULE)), Pid ! {set_levels, Levels}, ok. %%----------------------------------------------------------------------------- %% @return the current list of levels set in the logger %% @doc Get the current levels in the logger. %% @end %%----------------------------------------------------------------------------- -spec get_levels() -> {ok, Levels::[level()]} | {error, timeout}. get_levels() -> {ok, Pid} = maybe_start(whereis(?MODULE)), Ref = erlang:make_ref(), Pid ! {get_levels, Ref, self()}, receive {Ref, Levels} -> {ok, Levels} after 5000 -> {error, timeout} end. %%----------------------------------------------------------------------------- %% @param Filter the filter to set %% @return ok %% @doc Set the filter in the logger. %% Currently, a filter is a set of module names. If the filter is %% non-empty, then a log will be triggered only if the module %% being logged is in the filter list; otherwise, if the filter is %% empty, then the log will be triggered (if the level matches, as well) %% @end %%----------------------------------------------------------------------------- -spec set_filter(Filter::[module()]) -> ok. set_filter(Filter) -> {ok, Pid} = maybe_start(whereis(?MODULE)), Pid ! {set_filter, Filter}, ok. %%----------------------------------------------------------------------------- %% @return the current filter set in the logger %% @doc Get the current filter in the logger. %% @end %%----------------------------------------------------------------------------- -spec get_filter() -> {ok, Filter::[module()]} | {error, timeout}. get_filter() -> {ok, Pid} = maybe_start(whereis(?MODULE)), Ref = erlang:make_ref(), Pid ! {get_filter, Ref, self()}, receive {Ref, Filter} -> {ok, Filter} after 5000 -> {error, timeout} end. %%----------------------------------------------------------------------------- %% @param Sinks the list of sinks to set %% @return ok %% @doc Set the sinks in the logger. %% @end %%----------------------------------------------------------------------------- -spec set_sinks(Sinks::[sink()]) -> ok. set_sinks(Sinks) -> {ok, Pid} = maybe_start(whereis(?MODULE)), Pid ! {set_sinks, Sinks}, ok. %%----------------------------------------------------------------------------- %% @return the current list of sinks set in the logger %% @doc Get the current sinks in the logger. %% @end %%----------------------------------------------------------------------------- -spec get_sinks() -> {ok, Sinks::[sink()]} | {error, timeout}. get_sinks() -> {ok, Pid} = maybe_start(whereis(?MODULE)), Ref = erlang:make_ref(), Pid ! {get_sinks, Ref, self()}, receive {Ref, Sinks} -> {ok, Sinks} after 5000 -> {error, timeout} end. %% @hidden -spec console_log(Request::log_request()) -> ok. console_log(Request) -> {Location, Timestamp, Pid, Level, {Format, Args}} = Request, TimestampStr = make_timestamp(Timestamp), MFALStr = make_location(Location), PidLevelStr = io_lib:format("~p ~p", [Pid, Level]), MsgStr = io_lib:format(Format, Args), io:format(TimestampStr ++ " " ++ MFALStr ++ " " ++ PidLevelStr ++ ": " ++ MsgStr ++ "~n"). make_timestamp(Timestamp) -> {{Year, Month, Day}, {Hour, Minute, Second}} = Timestamp, io_lib:format("~p:~p:~pT~p:~p:~p", [ Year, Month, Day, Hour, Minute, Second ]). make_location(Location) -> {Module, Function, Arity, Line} = Location, io_lib:format("~p:~p/~p:~p", [ Module, Function, Arity, Line ]). %% %% Internal operations %% %% @private loop(#state{pid=StartingPid, config=Config} = State0) -> State = case StartingPid of undefined -> State0; _ -> StartingPid ! started, State0#state{pid=undefined} end, Levels = proplists:get_value(levels, Config, []), Filter = proplists:get_value(filter, Config, []), Sinks = proplists:get_value(sinks, Config, []), receive {get_levels, Ref, Pid} -> Pid ! {Ref, Levels}, loop(State); {get_filter, Ref, Pid} -> Pid ! {Ref, Filter}, loop(State); {get_sinks, Ref, Pid} -> Pid ! {Ref, Sinks}, loop(State); {set_levels, NewLevels} -> loop(State#state{config=[{levels, NewLevels} | lists:keydelete(levels, 1, Config)]}); {set_filter, NewFilter} -> loop(State#state{config=[{filter, NewFilter} | lists:keydelete(filter, 1, Config)]}); {set_sinks, NewSinks} -> loop(State#state{config=[{sinks, NewSinks} | lists:keydelete(sinks, 1, Config)]}); {_Location, _Time, _Pid, Level, _MsgFormat} = Request -> maybe_do_log(Sinks, Request, Level, Levels, Filter), loop(State); stop -> ok; _ -> loop(State) end. %% @private maybe_start(undefined) -> start(); maybe_start(Pid) -> {ok, Pid}. %% @private maybe_do_log(_Sinks, _Request, _Level, undefined, _Filter) -> ok; maybe_do_log(Sinks, {Location, _Time, _Pid, _Level, _MsgFormat} = Request, Level, Levels, Filter) -> case match_filter(Location, Filter) of true -> do_log_sinks(Sinks, Request, Level, Levels); _ -> ok end. %% @private match_filter(_Location, []) -> true; match_filter({Module, _Function, _Arity, _Line}, Filter) -> lists:member(Module, Filter). do_log_sinks([], _Request, _Level, _Levels) -> ok; do_log_sinks([Sink|Rest], Request, Level, Levels) -> do_log_sink(Sink, Request, Level, Levels), do_log_sinks(Rest, Request, Level, Levels). %% @private do_log_sink({Module, Function} = _Sink, Request, Level, Levels) -> case lists:member(Level, Levels) of true -> try Module:Function(Request) catch _:_ -> io:format( "An error occurred attempting to log to sink ~p:~p/1. request=~p~n", [Module, Function, Request] ) end; _ -> ok end. %% @private fill_defaults(Config, Defaults) -> fill_defaults(Config, Defaults, []). %% @private fill_defaults(_Config, [], Accum) -> Accum; fill_defaults(Config, [{K,_V}=H|T], Accum) -> case proplists:get_value(K, Config) of undefined -> fill_defaults(Config, T, [H|Accum]); Value -> fill_defaults(Config, T, [{K,Value}|Accum]) end.