Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable lager to use the OTP logger's event pipeline #524

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e1c4ed1
Add a `lager_use_logger` parse transform to rewrite lager calls to lo…
Vagabond Feb 3, 2019
f1c7e3a
Couple fixes
Vagabond Feb 3, 2019
7aaad5c
Handle log messages with no format arguments
Vagabond Feb 3, 2019
a68e456
Backwards
Vagabond Feb 3, 2019
ad25bf8
Add an env var to stop lager actually booting
Vagabond Feb 3, 2019
62d95d2
Add a module to convert logger formatters to lager formatters
Vagabond Feb 3, 2019
0970513
Clear colors at end of line
Vagabond Feb 3, 2019
e53ecb0
Typo
Vagabond Feb 3, 2019
8a2cf8d
Add some formatters for the standard OTP reports
Vagabond Feb 3, 2019
0176941
Fallback for unhandled reports
Vagabond Feb 3, 2019
c793c03
Tweak
Vagabond Feb 3, 2019
9b8ee23
Honor supervisor message suppression config
Vagabond Feb 3, 2019
4f8d245
Feh
Vagabond Feb 3, 2019
9ed7ee8
Add supervisor error reports
Vagabond Feb 4, 2019
8ac924b
Debugging
Vagabond Feb 4, 2019
7e247dc
Typo:
Vagabond Feb 4, 2019
0ba40a2
Add application start
Vagabond Feb 4, 2019
24651de
Use the right key names
Vagabond Feb 4, 2019
0e07296
Add application stops
Vagabond Feb 4, 2019
5667c95
Try to switch to using a report_cb
Vagabond Feb 4, 2019
7b8d2c5
More fighting with report_cb
Vagabond Feb 4, 2019
ce11e41
Use report_cb from config or from metadata
Vagabond Feb 4, 2019
7af01cc
Try to supress log messages better
Vagabond Feb 9, 2019
566d0f3
Fix
Vagabond Feb 9, 2019
60f6b38
Typo
Vagabond Feb 9, 2019
2259907
Fix
Vagabond Feb 9, 2019
3d38191
Fix
Vagabond Feb 9, 2019
3c275e4
Use better function
Vagabond Feb 24, 2019
0803df0
How the heck did this slip in?
Vagabond Mar 1, 2020
0dff911
Add some docs
Vagabond Mar 2, 2020
c62732d
WIP function to turn lager config into logger config
Vagabond Mar 2, 2020
6746643
Rework logger configuration and add logger config generator
Vagabond Mar 2, 2020
244463e
logger_std_h supports file rotation better than disk_log, so use that
Vagabond Mar 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,69 @@ Example Usage:
:lager.warning('Some message with a term: ~p', [term])
```

Integration with OTP's Logger
-----------------------------

Now that OTP includes a modern log event pipeline, the Lager project has decided
to work on winding down Lager development in favor of unifying the Erlang
ecosystem behind Logger. To that end, Lager now supports being configured to use
its parse transform to rewrite lager calls into logger calls. To enable this
mode, the following changes are required:

In sys.config:
```
[
{kernel,
[{logger,
[{handler, default, logger_std_h,
#{formatter => {lager_logger_formatter, #{report_cb => fun lager_logger_formatter:report_cb/1}}}},
]}]},
...
{lager, [
{lager_use_logger, true},
...
]}
]
```

The logger stanza changes are not needed if you wish to use logger's default
formatter.

In your applications top level rebar.config (probably requires rebar3):

```
{overrides, [{add, [{erl_opts, [{lager_use_logger, true}]}]}]}.
```

This will force the parse transform configuration to apply to all the
dependencies as well. Make sure you've defined the lager version in your
toplevel application's rebar.config and that the version is high enough to
support these options. A toplevel dependency will override any lager
dependencies in any of your application's dependencies and thus ensure the parse
transform is the right version.

To generate a logger configuration from your lager configuration you can do:

```
lager:generate_logger_configuration()
```

If you wish to simply use your existing lager configuration as is, and have
lager configure logger you can, in your sys.config in the lager stanza:

```
{lager, [
{configure_logger, true},
...
]}
```

Alternatively you can use:

```
lager:configure_logger()
```

3.x Changelog
-------------
3.6.8 - 21 December 2018
Expand Down
61 changes: 60 additions & 1 deletion src/lager.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
get_loglevel/1, get_loglevel/2, set_loglevel/2, set_loglevel/3, set_loglevel/4, get_loglevels/1,
update_loglevel_config/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]).
do_log/9, do_log/10, do_log_unsafe/10, pr/2, pr/3, pr_stacktrace/1, pr_stacktrace/2,
generate_logger_config/0, configure_logger/0]).

-type log_level() :: none | debug | info | notice | warning | error | critical | alert | emergency.
-type log_level_number() :: 0..7.
Expand Down Expand Up @@ -689,6 +690,64 @@ rotate_handler(Handler) ->
rotate_handler(Handler, Sink) ->
gen_event:call(Sink, Handler, rotate, ?ROTATE_TIMEOUT).

generate_logger_config() ->
Handlers = application:get_env(lager, handlers, lager_app:default_handlers()),
{Level, NewHandlers} = generate_logger_handlers(Handlers, {notice, []}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of "translating" Lager handlers (which will not support custom handlers anyway) to logger handlers, add new handler that will forward all messages from the logger to lager's gen_event as a compatibility layer.

{kernel, [{logger_level, Level}, {logger, NewHandlers}]}.

configure_logger() ->
Handlers = application:get_env(lager, handlers, lager_app:default_handlers()),
WhitelistedLoggerHandlers = application:get_env(lager, whitelisted_logger_handlers, []),
[ ok = logger:remove_handler(Id) || #{id := Id} <- logger:get_handler_config(), not lists:member(Id, WhitelistedLoggerHandlers) ],
{Level, NewHandlers} = generate_logger_handlers(Handlers, {notice, []}),
logger:set_primary_config(maps:merge(logger:get_primary_config(), #{level => Level})),
[ ok = logger:add_handler(HandlerId, HandlerModule, HandlerConfig) || {handler, HandlerId, HandlerModule, HandlerConfig} <- NewHandlers ],
ok.

generate_logger_handlers([], Acc) ->
Acc;
generate_logger_handlers([{lager_console_backend, Config}|Tail], {CurrentLevel, Acc}) ->
Level = proplists:get_value(level, Config, info),
Formatter = proplists:get_value(formatter, Config, lager_default_formatter),
FormatterConfig = proplists:get_value(formatter_config, Config, []),
Handler = {handler, console, logger_std_h, #{level => Level, formatter =>
{lager_logger_formatter, #{report_cb => fun lager_logger_formatter:report_cb/1,
formatter => Formatter,
formatter_config => FormatterConfig}}}},
NewLevel = case lager_util:level_to_num(Level) > lager_util:level_to_num(CurrentLevel) of
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of that you can use logger:compare_levels/2.

true ->
Level;
false ->
CurrentLevel
end,
generate_logger_handlers(Tail, {NewLevel, [Handler|Acc]});
generate_logger_handlers([{lager_file_backend, Config}|Tail], {CurrentLevel, Acc}) ->
Level = proplists:get_value(level, Config, info),
File = proplists:get_value(file, Config),
LogRoot = application:get_env(lager, log_root, ""),
Size = proplists:get_value(size, Config),
Count = proplists:get_value(count, Config),
Formatter = proplists:get_value(formatter, Config, lager_default_formatter),
FormatterConfig = proplists:get_value(formatter_config, Config, []),
%% XXX I don't think disk_log is suitable as it is, we should provide a logger compatible version of
%% lager's file_backend and try to patch disk_log upstream
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Handler = {handler, list_to_atom(File), logger_disk_log_h, #{level => Level,
file => filename:join(LogRoot, File),
max_no_files => Count,
max_no_bytes => Size,
formatter =>
{lager_logger_formatter, #{report_cb => fun lager_logger_formatter:report_cb/1,
formatter => Formatter,
formatter_config => FormatterConfig}}}},
NewLevel = case lager_util:level_to_num(Level) > lager_util:level_to_num(CurrentLevel) of
true ->
Level;
false ->
CurrentLevel
end,
generate_logger_handlers(Tail, {NewLevel, [Handler|Acc]}).


%% @private
trace_func(#trace_func_state_v1{pid=Pid, level=Level, format_string=Fmt}=FuncState, Event, ProcState) ->
lager:log(Level, Pid, Fmt, [Event, ProcState]),
Expand Down
27 changes: 21 additions & 6 deletions src/lager_app.erl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
start_handler/3,
configure_sink/2,
stop/1,
boot/1]).
boot/1,
default_handlers/0]).

%% The `application:get_env/3` compatibility wrapper was useful
%% for other modules in r15 and before
Expand Down Expand Up @@ -226,11 +227,22 @@ get_env(Application, Key, Default) ->

start(_StartType, _StartArgs) ->
{ok, Pid} = lager_sup:start_link(),
SavedHandlers = boot(),
_ = boot('__all_extra'),
_ = boot('__traces'),
clean_up_config_checks(),
{ok, Pid, SavedHandlers}.
case application:get_env(lager, lager_use_logger, false) of
false ->
SavedHandlers = boot(),
_ = boot('__all_extra'),
_ = boot('__traces'),
clean_up_config_checks(),
{ok, Pid, SavedHandlers};
true ->
case application:get_env(lager, configure_logger, false) of
true ->
ok = lager:configure_logger();
false ->
ok
end,
{ok, Pid}
end.

boot() ->
%% Handle the default sink.
Expand Down Expand Up @@ -276,6 +288,9 @@ stop(Handlers) ->
error_logger:add_report_handler(Handler)
end, Handlers).

default_handlers() ->
?DEFAULT_HANDLER_CONF.

expand_handlers([]) ->
[];
expand_handlers([{lager_file_backend, [{Key, _Value}|_]=Config}|T]) when is_atom(Key) ->
Expand Down
119 changes: 119 additions & 0 deletions src/lager_logger_formatter.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
-module(lager_logger_formatter).

%% convert logger formatter calls into lager formatter ones

-export([report_cb/1, format/2]).%, check_config/1]).

report_cb(#{label := {gen_server, terminate}, name := Name, reason := Reason}) ->
Formatted = error_logger_lager_h:format_reason(Reason),
{"gen_server ~w terminated with reason: ~s", [Name, Formatted]};
report_cb(#{label := {gen_fsm, terminate}, name := Name, state_name := StateName, reason := Reason}) ->
Formatted = error_logger_lager_h:format_reason(Reason),
{"gen_fsm ~w in state ~w terminated with reason: ~s", [Name, StateName, Formatted]};
report_cb(#{label := {gen_event, terminate}, name := Name, handler := Handler, reason := Reason}) ->
Formatted = error_logger_lager_h:format_reason(Reason),
{"gen_event ~w installed in ~w terminated with reason: ~s", [Handler, Name, Formatted]};
report_cb(#{label := {gen_statem, terminate}, name := Name, reason := Reason}) ->
Formatted = error_logger_lager_h:format_reason(Reason),
%% XXX I can't find the FSM statename in the error report, maybe it should be added
{"gen_statem ~w terminated with reason: ~s", [Name, Formatted]};
report_cb(#{msg := {report, #{label := {Behaviour, no_handle_info}, mod := Mod, msg := Msg}}}) ->
{"undefined handle_info for ~p in ~s ~p", [Msg, Behaviour, Mod]};
report_cb(#{label := {supervisor, progress}, report := Report}) ->
case application:get_env(lager, suppress_supervisor_start_stop, false) of
true ->
{"", []};
false ->
{supervisor, Name} = lists:keyfind(supervisor, 1, Report),
{started, Started} = lists:keyfind(started, 1, Report),
case lists:keyfind(id, 1, Started) of
false ->
%% supervisor itself starting
{mfa, {Module, Function, Args}} = lists:keyfind(mfa, 1, Started),
{pid, Pid} = lists:keyfind(pid, 1, Started),
{"Supervisor ~w started as ~p at pid ~w", [Name, error_logger_lager_h:format_mfa({Module, Function, Args}), Pid]};
{id, ChildID} ->
case lists:keyfind(pid, 1, Started) of
{pid, Pid} ->
{"Supervisor ~w started child ~p at pid ~w", [Name, ChildID, Pid]};
false ->
%% children is a list of pids for some reason? and we only get the count
{nb_children, ChildCount} = lists:keyfind(nb_children, 1, Started),
{"Supervisor ~w started ~b children ~p", [Name, ChildCount, ChildID]}
end
end
end;
report_cb(#{label := {supervisor, _Error}, report := Report}) ->
{supervisor, Name} = lists:keyfind(supervisor, 1, Report),
{reason, Reason} = lists:keyfind(reason, 1, Report),
Formatted = error_logger_lager_h:format_reason(Reason),
{errorContext, ErrorContext} = lists:keyfind(errorContext, 1, Report),
{offender, Offender} = lists:keyfind(offender, 1, Report),
case lists:keyfind(mod, 1, Offender) of
{mod, _Mod} ->
{pid, Pid} = lists:keyfind(pid, 1, Offender),
%% this comes from supervisor_bridge
{"Supervisor ~w had ~p ~p with reason ~s", [Name, Pid, ErrorContext, Formatted]};
false ->
{id, ChildID} = lists:keyfind(id, 1, Offender),
case lists:keyfind(pid, 1, Offender) of
{pid, Pid} ->
{"Supervisor ~w had ~p ~p ~p with reason ~s", [Name, ChildID, Pid, ErrorContext, Formatted]};
false ->
{"Supervisor ~w had ~p ~p with reason ~s", [Name, ChildID, ErrorContext, Formatted]}
end
end;
report_cb(#{label := {application_controller, progress}, report := Report}) ->
case application:get_env(lager, suppress_application_start_stop, false) of
true -> {"", []};
false ->
{application, Name} = lists:keyfind(application, 1, Report),
{started_at, Node} = lists:keyfind(started_at, 1, Report),
{"Application ~w started on node ~w", [Name, Node]}
end;
report_cb(#{label := {application_controller, exit}, report := Report}) ->
{exited, Reason} = lists:keyfind(exited, 1, Report),
case application:get_env(lager, suppress_application_start_stop) of
{ok, true} when Reason == stopped ->
{"", []};
_ ->
{application, Name} = lists:keyfind(application, 1, Report),
Formatted = error_logger_lager_h:format_reason(Reason),
{"Application ~w exited with reason: ~s", [Name, Formatted]}
end.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will fail on custom reports, ex.:

?LOG_INFO(#{http_reponse => 200, request_duration => Duration})

%% TODO handle proc_lib crash

format(#{msg := {report, _Report}, meta := Metadata} = Event, #{report_cb := Fun} = Config) when is_function(Fun, 1); is_function(Fun, 2) ->
format(Event#{meta => Metadata#{report_cb => Fun}}, maps:remove(report_cb, Config));
format(#{level := _Level, msg := {report, Report}, meta := #{report_cb := Fun}} = Event, Config) when is_function(Fun, 1) ->
case Fun(Report) of
{"", []} -> "";
{Format, Args} when is_list(Format), is_list(Args) ->
format(Event#{msg => {Format, Args}}, Config)
end;
format(#{level := Level, msg := {string, String}, meta := Metadata}, Config) ->
do_format(Level, String, Metadata, Config);
format(#{level := Level, msg := {FmtStr, FmtArgs}, meta := Metadata}, Config) ->
Msg = lager_format:format(FmtStr, FmtArgs, maps:get(max_size, Config, 1024)),
do_format(Level, Msg, Metadata, Config).

do_format(Level, Msg, Metadata, Config) ->
FormatModule = maps:get(formatter, Config, lager_default_formatter),
Timestamp = maps:get(time, Metadata),
MegaSecs = Timestamp div 1000000000000,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make these numbers macros to improve readability and maintenance sanity, please.

Secs = (Timestamp rem 1000000000000) div 1000000,
MicroSecs = (Timestamp rem 1000000000000) rem 1000000,
{Colors, End} = case maps:get(colors, Config, false) of
true ->
{application:get_env(lager, colors, []), "\e[0m"};
false ->
{[], ""}
end,
[FormatModule:format(lager_msg:new(Msg, {MegaSecs, Secs, MicroSecs}, Level, convert_metadata(Metadata), []), maps:get(formatter_config, Config, []), Colors), End].

convert_metadata(Metadata) ->
maps:fold(fun(mfa, {Module, Function, Arity}, Acc) ->
[{module, Module}, {function, Function}, {arity, Arity}|Acc];
(K, V, Acc) ->
[{K, V}|Acc]
end, [], Metadata).
41 changes: 23 additions & 18 deletions src/lager_sup.erl
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,29 @@ start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
%% set up the config, is safe even during relups
lager_config:new(),
%% TODO:
%% Always start lager_event as the default and make sure that
%% other gen_event stuff can start up as needed
%%
%% Maybe a new API to handle the sink and its policy?
Children = [
{lager, {gen_event, start_link, [{local, lager_event}]},
permanent, 5000, worker, dynamic},
{lager_handler_watcher_sup, {lager_handler_watcher_sup, start_link, []},
permanent, 5000, supervisor, [lager_handler_watcher_sup]}],

CrashLog = decide_crash_log(application:get_env(lager, crash_log, false)),

{ok, {{one_for_one, 10, 60},
Children ++ CrashLog
}}.
case application:get_env(lager, lager_use_logger, false) of
true ->
{ok, {{one_for_one, 10, 60}, []}};
false ->
%% set up the config, is safe even during relups
lager_config:new(),
%% TODO:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: probably should delete or update this comment

%% Always start lager_event as the default and make sure that
%% other gen_event stuff can start up as needed
%%
%% Maybe a new API to handle the sink and its policy?
Children = [
{lager, {gen_event, start_link, [{local, lager_event}]},
permanent, 5000, worker, dynamic},
{lager_handler_watcher_sup, {lager_handler_watcher_sup, start_link, []},
permanent, 5000, supervisor, [lager_handler_watcher_sup]}],

CrashLog = decide_crash_log(application:get_env(lager, crash_log, false)),

{ok, {{one_for_one, 10, 60},
Children ++ CrashLog
}}
end.

validate_positive({ok, Val}, _Default) when is_integer(Val) andalso Val >= 0 ->
Val;
Expand Down
Loading