1%% This Source Code Form is subject to the terms of the Mozilla Public
2%% License, v. 2.0. If a copy of the MPL was not distributed with this
3%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4%%
5%% Copyright (c) 2019-2021 VMware, Inc. or its affiliates.  All rights reserved.
6%%
7
8%% @author The RabbitMQ team
9%% @copyright 2019-2021 VMware, Inc. or its affiliates.
10%%
11%% @doc
12%% This module manages the configuration of the Erlang Logger facility. In
13%% other words, it translates the RabbitMQ logging configuration (in the
14%% Cuttlefish format or classic Erlang-term-based configuration) into Erlang
15%% Logger handler setups.
16%%
17%% Configuring the Erlang Logger is done in two steps:
18%% <ol>
19%% <li>Logger handler configurations are created based on the configuration
20%% and the context (see {@link //rabbit_common/rabbit_env}).</li>
21%% <li>Created handlers are installed (i.e. they become active). Any handlers
22%% previously installed by this module are removed.</li>
23%% </ol>
24%%
25%% It also takes care of setting the `$ERL_CRASH_DUMP' variable to enable
26%% Erlang core dumps.
27%%
28%% Note that before this module handles the Erlang Logger, {@link
29%% //rabbitmq_prelaunch/rabbit_prelaunch_early_logging} configures basic
30%% logging to have messages logged as soon as possible during RabbitMQ
31%% startup.
32%%
33%% == How to configure RabbitMQ logging ==
34%%
35%% RabbitMQ supports a main/default logging output and per-category outputs.
36%% An output is a combination of a destination (a text file or stdout for
37%% example) and a message formatted (e.g. plain text or JSON).
38%%
39%% Here is the Erlang-term-based configuration expected and supported by this
40%% module:
41%%
42%% ```
43%% {rabbit, [
44%%   {log_root, string()},
45%%   {log, [
46%%     {categories, [
47%%       {default, [
48%%         {level, Level}
49%%       ]},
50%%       {CategoryName, [
51%%         {level, Level},
52%%         {file, Filename}
53%%       ]}
54%%     ]},
55%%
56%%     {console, [
57%%       {level, Level},
58%%       {enabled, boolean()}
59%%     ]},
60%%
61%%     {exchange, [
62%%       {level, Level},
63%%       {enabled, boolean()}
64%%     ]},
65%%
66%%     {file, [
67%%       {level, Level},
68%%       {file, Filename | false},
69%%       {date, RotationDateSpec},
70%%       {size, RotationSize},
71%%       {count, RotationCount},
72%%     ]},
73%%
74%%     {journald, [
75%%       {level, Level},
76%%       {enabled, boolean()},
77%%       {fields, proplists:proplist()}
78%%     ]}
79%%
80%%     {syslog, [
81%%       {level, Level},
82%%       {enabled, boolean()}
83%%     ]}
84%%   ]}
85%% ]}.
86%%
87%% Level = logger:level().
88%% Filename = file:filename().
89%% RotationDateSpec = string(). % Pattern format used by newsyslog.conf(5).
90%% RotationSize = non_neg_integer() | infinity.
91%% RotationCount = non_neg_integer().
92%% '''
93%%
94%% See `priv/schema/rabbit.schema' for the definition of the Cuttlefish
95%% configuration schema.
96
97-module(rabbit_prelaunch_logging).
98
99-include_lib("kernel/include/logger.hrl").
100-include_lib("rabbit_common/include/logging.hrl").
101
102-export([setup/1,
103         set_log_level/1,
104         log_locations/0]).
105
106-ifdef(TEST).
107-export([clear_config_run_number/0,
108         get_less_severe_level/2]).
109-endif.
110
111-export_type([log_location/0]).
112
113-type log_location() :: file:filename() | string().
114%% A short description of an output.
115%%
116%% If the output is the console, the location is either `"<stdout>"' or
117%% `"<stderr>"'.
118%%
119%% If the output is an exchange, the location is the string `"exchange:"' with
120%% the exchange name appended.
121%%
122%% If the output is a file, the location is the absolute filename.
123%%
124%% If the output is journald, the location is `"<journald>"'.
125%%
126%% If the output is syslog, the location is the string `"syslog:"' with the
127%% syslog server hostname appended.
128
129-type category_name() :: atom().
130%% The name of a log category.
131%% Erlang Logger uses the concept of "domain" which is an ordered list of
132%% atoms. A category is mapped to the domain `[?RMQLOG_SUPER_DOMAIN_NAME,
133%% Category]'. In other words, a category is a subdomain of the `rabbitmq'
134%% domain.
135
136-type console_props() :: [{level, logger:level()} |
137                          {enabled, boolean()} |
138                          {stdio, stdout | stderr} |
139                          {formatter, {atom(), term()}}].
140%% Console properties are the parameters in the configuration file for a
141%% console-based handler.
142
143-type exchange_props() :: [{level, logger:level()} |
144                           {enabled, boolean()} |
145                           {formatter, {atom(), term()}}].
146%% Exchange properties are the parameters in the configuration file for an
147%% exchange-based handler.
148
149-type file_props() :: [{level, logger:level()} |
150                       {file, file:filename() | false} |
151                       {date, string()} |
152                       {size, non_neg_integer()} |
153                       {count, non_neg_integer()} |
154                       {formatter, {atom(), term()}}].
155%% File properties are the parameters in the configuration file for a
156%% file-based handler.
157
158-type journald_props() :: [{level, logger:level()} |
159                           {enabled, boolean()} |
160                           {fields, proplists:proplist()}].
161%% journald properties are the parameters in the configuration file for a
162%% journald-based handler.
163
164-type syslog_props() :: [{level, logger:level()} |
165                         {enabled, boolean()} |
166                         {formatter, {atom(), term()}}].
167%% Syslog properties are the parameters in the configuration file for a
168%% syslog-based handler.
169
170-type main_log_env() :: [{console, console_props()} |
171                         {exchange, exchange_props()} |
172                         {file, file_props()} |
173                         {journald, journald_props()} |
174                         {syslog, syslog_props()}].
175%% The main log environment is the parameters in the configuration file for
176%% the main log handler (i.e. where all messages go by default).
177
178-type per_cat_env() :: [{level, logger:level()} |
179                        {file, file:filename()}].
180%% A per-category log environment is the parameters in the configuration file
181%% for a specific category log handler. There can be one per category.
182
183-type default_cat_env() :: [{level, logger:level()}].
184%% The `default' category log environment is special (read: awkward) in the
185%% configuration file. It is used to change the log level of the main log
186%% handler.
187
188-type log_app_env() :: [main_log_env() |
189                        {categories, [{default, default_cat_env()} |
190                                      {category_name(), per_cat_env()}]}].
191%% The value for the `log' key in the `rabbit' application environment.
192
193-type global_log_config() :: #{level => logger:level() | all | none,
194                               outputs := [logger:handler_config()]}.
195%% This is the internal structure used to prepare the handlers for the
196%% main/global messages (i.e. not marked with a specific category).
197
198-type per_cat_log_config() :: global_log_config().
199%% This is the internal structure used to prepare the handlers for
200%% category-specific messages.
201
202-type log_config() :: #{global := global_log_config(),
203                        per_category := #{
204                          category_name() => per_cat_log_config()}}.
205%% This is the internal structure to store the global and per-category
206%% configurations, to prepare the final handlers.
207
208-type handler_key() :: atom().
209%% Key used to deduplicate handlers before they are installed in Logger.
210
211-type id_assignment_state() :: #{config_run_number := pos_integer(),
212                                 next_file := pos_integer()}.
213%% State used while assigning IDs to handlers.
214
215-spec setup(rabbit_env:context()) -> ok.
216%% @doc
217%% Configures or reconfigures logging.
218%%
219%% The logging framework is the builtin Erlang Logger API. The configuration
220%% is based on the configuration file and the environment.
221%%
222%% In addition to logging, it sets the `$ERL_CRASH_DUMP' environment variable
223%% to enable Erlang crash dumps.
224%%
225%% @param Context the RabbitMQ context (see {@link
226%% //rabbitmq_prelaunch/rabbit_prelaunch:get_context/0}).
227
228setup(Context) ->
229    ?LOG_DEBUG("\n== Logging ==",
230               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
231    ok = compute_config_run_number(),
232    ok = set_ERL_CRASH_DUMP_envvar(Context),
233    ok = configure_logger(Context).
234
235-spec set_log_level(logger:level()) -> ok | {error, term()}.
236%% @doc
237%% Changes the log level.
238%%
239%% The log level is changed for the following layers of filtering:
240%% <ul>
241%% <li>the primary log level</li>
242%% <li>the module log level(s)</li>
243%% <li>the handler log level(s)</li>
244%% </ul>
245%%
246%% @param Level the new log level.
247
248set_log_level(Level) ->
249    %% Primary log level.
250    ?LOG_DEBUG(
251       "Logging: changing primary log level to ~s", [Level],
252       #{domain => ?RMQLOG_DOMAIN_GLOBAL}),
253    logger:set_primary_config(level, Level),
254
255    %% Per-module log level.
256    lists:foreach(
257      fun({Module, _}) ->
258              ?LOG_DEBUG(
259                 "Logging: changing '~s' module log level to ~s",
260                 [Module, Level],
261                 #{domain => ?RMQLOG_DOMAIN_GLOBAL}),
262              _ = logger:set_module_level(Module, Level)
263      end, logger:get_module_level()),
264
265    %% Per-handler log level.
266    %% In the handler, we have the "top-level" log level for that handler, plus
267    %% per-domain (per-category) levels inside a filter. We need to change all
268    %% of them to the new level.
269    lists:foreach(
270      fun
271          (#{id := Id, filters := Filters, config := Config}) ->
272              ?LOG_DEBUG(
273                 "Logging: changing '~s' handler log level to ~s",
274                 [Id, Level],
275                 #{domain => ?RMQLOG_DOMAIN_GLOBAL}),
276              Filters1 = lists:map(
277                           fun
278                               %% We only modify the filter we know about.
279                               %% The argument to that filter is of the form:
280                               %%     #{CatName => Level}
281                               %% (where CatName if an atom())
282                               %%
283                               %% We don't change the level for a category if
284                               %% it is set to 'none' because it means the
285                               %% category is filtered out by this filter.
286                               ({?FILTER_NAME, {Fun, Arg}}) when is_map(Arg) ->
287                                   Arg1 = maps:map(
288                                            fun
289                                                (_, none) -> none;
290                                                (_, _)    -> Level
291                                            end, Arg),
292                                   {?FILTER_NAME, {Fun, Arg1}};
293                               %% We also change what we do with Erlang
294                               %% progress reports.
295                               ({progress_reports, {Fun, _}}) ->
296                                   Action = case Level of
297                                                debug -> log;
298                                                _     -> stop
299                                            end,
300                                   {progress_reports, {Fun, Action}};
301                               %% Other filters are left untouched.
302                               (Filter) ->
303                                   Filter
304                           end, Filters),
305              %% If the log level is set to `debug', we turn off burst limit to
306              %% make sure all debug messages make it.
307              Config1 = adjust_burst_limit(Config, Level),
308              logger:set_handler_config(Id, filters, Filters1),
309              logger:set_handler_config(Id, config, Config1),
310              logger:set_handler_config(Id, level, Level),
311              ok;
312          (#{id := Id, config := Config}) ->
313              ?LOG_DEBUG(
314                 "Logging: changing '~s' handler log level to ~s",
315                 [Id, Level],
316                 #{domain => ?RMQLOG_DOMAIN_GLOBAL}),
317              %% If the log level is set to `debug', we turn off burst limit to
318              %% make sure all debug messages make it.
319              Config1 = adjust_burst_limit(Config, Level),
320              logger:set_handler_config(Id, config, Config1),
321              logger:set_handler_config(Id, level, Level),
322              ok
323      end, logger:get_handler_config()),
324    ok.
325
326-spec log_locations() -> [file:filename() | string()].
327%% @doc
328%% Returns the list of output locations.
329%%
330%% For file-based handlers, the absolute filename is returned.
331%%
332%% For console-based handlers, a string literal is returned; either
333%% `"<stdout>"' or `"<stderr>"'.
334%%
335%% For exchange-based handlers, a string of the form `"exchange:Exchange"' is
336%% returned, where `Exchange' is the name of the exchange.
337%%
338%% For journald-based handlers, a string literal is returned; `"<journald>"'.
339%%
340%% For syslog-based handlers, a string of the form `"syslog:Hostname"' is
341%% returned, where `Hostname' is either the hostname of the remote syslog
342%% server, or an empty string if none were configured (which means log to
343%% localhost).
344%%
345%% @returns the list of output locations.
346%%
347%% @see log_location()
348
349log_locations() ->
350    Handlers = logger:get_handler_config(),
351    log_locations(Handlers, []).
352
353log_locations([#{module := Mod,
354                 config := #{type := file,
355                             file := Filename}} | Rest],
356              Locations)
357  when ?IS_STD_H_COMPAT(Mod) ->
358    Locations1 = add_once(Locations, Filename),
359    log_locations(Rest, Locations1);
360log_locations([#{module := Mod,
361                 config := #{type := standard_io}} | Rest],
362              Locations)
363  when ?IS_STD_H_COMPAT(Mod) ->
364    Locations1 = add_once(Locations, "<stdout>"),
365    log_locations(Rest, Locations1);
366log_locations([#{module := Mod,
367                 config := #{type := standard_error}} | Rest],
368              Locations)
369  when ?IS_STD_H_COMPAT(Mod) ->
370    Locations1 = add_once(Locations, "<stderr>"),
371    log_locations(Rest, Locations1);
372log_locations([#{module := systemd_journal_h} | Rest],
373              Locations) ->
374    Locations1 = add_once(Locations, "<journald>"),
375    log_locations(Rest, Locations1);
376log_locations([#{module := syslog_logger_h} | Rest],
377              Locations) ->
378    Host = application:get_env(syslog, dest_host, ""),
379    Locations1 = add_once(
380                   Locations,
381                   rabbit_misc:format("syslog:~s", [Host])),
382    log_locations(Rest, Locations1);
383log_locations([#{module := rabbit_logger_exchange_h,
384                 config := #{exchange := Exchange}} | Rest],
385              Locations) ->
386    Locations1 = add_once(
387                   Locations,
388                   rabbit_misc:format("exchange:~p", [Exchange])),
389    log_locations(Rest, Locations1);
390log_locations([_ | Rest], Locations) ->
391    log_locations(Rest, Locations);
392log_locations([], Locations) ->
393    lists:sort(Locations).
394
395add_once(Locations, Location) ->
396    case lists:member(Location, Locations) of
397        false -> [Location | Locations];
398        true  -> Locations
399    end.
400
401%% -------------------------------------------------------------------
402%% ERL_CRASH_DUMP setting.
403%% -------------------------------------------------------------------
404
405-spec set_ERL_CRASH_DUMP_envvar(rabbit_env:context()) -> ok.
406
407set_ERL_CRASH_DUMP_envvar(Context) ->
408    case os:getenv("ERL_CRASH_DUMP") of
409        false ->
410            LogBaseDir = get_log_base_dir(Context),
411            ErlCrashDump = filename:join(LogBaseDir, "erl_crash.dump"),
412            ?LOG_DEBUG(
413              "Setting $ERL_CRASH_DUMP environment variable to \"~ts\"",
414              [ErlCrashDump],
415              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
416            os:putenv("ERL_CRASH_DUMP", ErlCrashDump),
417            ok;
418        ErlCrashDump ->
419            ?LOG_DEBUG(
420              "$ERL_CRASH_DUMP environment variable already set to \"~ts\"",
421              [ErlCrashDump],
422              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
423            ok
424    end.
425
426-spec get_log_base_dir(rabbit_env:context()) -> file:filename().
427%% @doc
428%% Returns the log base directory.
429%%
430%% The precedence is:
431%% <ol>
432%% <li>the $RABBITMQ_LOG_BASE variable if overriden in the environment</li>
433%% <li>the value of `log_root' in the application environment</li>
434%% <li>the default value</li>
435%% </ol>
436%%
437%% @param Context the RabbitMQ context (see {@link
438%% //rabbitmq_prelaunch/rabbit_prelaunch:get_context/0}).
439%% @returns an absolute path to a directory.
440
441get_log_base_dir(#{log_base_dir := LogBaseDirFromEnv} = Context) ->
442    case rabbit_env:has_var_been_overridden(Context, log_base_dir) of
443        false -> application:get_env(rabbit, log_root, LogBaseDirFromEnv);
444        true  -> LogBaseDirFromEnv
445    end.
446
447%% -------------------------------------------------------------------
448%% Logger's handlers configuration.
449%% -------------------------------------------------------------------
450
451-define(CONFIG_RUN_NUMBER_KEY, {?MODULE, config_run_number}).
452
453-spec compute_config_run_number() -> ok.
454%% @doc
455%% Compute the next configuration run number.
456%%
457%% We use a "configuration run number" to distinguish previously installed
458%% handlers (if any) from the ones we want to install in a subsequent call of
459%% {@link setup/1}.
460%%
461%% The configuration run number appears in the generated IDs for handlers.
462%% This is how we can filter old ones.
463%%
464%% @returns an positive integer.
465
466compute_config_run_number() ->
467    RunNum = persistent_term:get(?CONFIG_RUN_NUMBER_KEY, 0),
468    ok = persistent_term:put(?CONFIG_RUN_NUMBER_KEY, RunNum + 1).
469
470-spec get_config_run_number() -> pos_integer().
471
472get_config_run_number() ->
473    persistent_term:get(?CONFIG_RUN_NUMBER_KEY).
474
475-ifdef(TEST).
476-spec clear_config_run_number() -> ok.
477%% @doc
478%% Clears the recorded configuration run number.
479%%
480%% In testsuites, we want to be able to reset that number to 1 because
481%% testcases have expectations on handler IDs.
482
483clear_config_run_number() ->
484    _ = persistent_term:erase(?CONFIG_RUN_NUMBER_KEY),
485    ok.
486-endif.
487
488-spec configure_logger(rabbit_env:context()) -> ok.
489
490configure_logger(Context) ->
491    %% Configure main handlers.
492    %% We distinguish them by their type and possibly other
493    %% parameters (file name, syslog settings, etc.).
494    LogConfig0 = get_log_configuration_from_app_env(),
495    LogConfig1 = handle_default_and_overridden_outputs(LogConfig0, Context),
496    LogConfig2 = apply_log_levels_from_env(LogConfig1, Context),
497    LogConfig3 = make_filenames_absolute(LogConfig2, Context),
498    LogConfig4 = configure_formatters(LogConfig3, Context),
499
500    %% At this point, the log configuration is complete: we know the global
501    %% parameters as well as the per-category settings.
502    %%
503    %% Now, we turn that into a map of handlers. We use a map to deduplicate
504    %% handlers. For instance, if the same file is used for global logging and
505    %% a specific category.
506    %%
507    %% The map is then converted to a list, once the deduplication is done and
508    %% IDs are assigned to handlers.
509    Handlers = create_logger_handlers_conf(LogConfig4),
510    ?LOG_DEBUG(
511       "Logging: logger handlers:~n  ~p", [Handlers],
512       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
513
514    %% We can now install the new handlers. The function takes care of
515    %% removing previously configured handlers (after installing the new
516    %% ones to ensure we don't loose a message).
517    ok = install_handlers(Handlers),
518
519    %% Let's log a message per log level (if debug logging is enabled). This
520    %% is handy if the user wants to verify the configuration is what he
521    %% expects.
522    ok = maybe_log_test_messages(LogConfig3).
523
524-spec get_log_configuration_from_app_env() -> log_config().
525
526get_log_configuration_from_app_env() ->
527    %% The log configuration in the Cuttlefish configuration file or the
528    %% application environment is not structured logically. This functions is
529    %% responsible for extracting the configuration and organize it. If one day
530    %% we decide to fix the configuration structure, we just have to modify
531    %% this function and normalize_*().
532    Env = get_log_app_env(),
533    EnvWithoutCats = proplists:delete(categories, Env),
534    DefaultAndCatProps = proplists:get_value(categories, Env, []),
535    DefaultProps = proplists:get_value(default, DefaultAndCatProps, []),
536    CatProps = proplists:delete(default, DefaultAndCatProps),
537
538    %% This "normalization" turns the RabbitMQ-specific configuration into a
539    %% structure which stores Logger handler configurations. That structure is
540    %% later modified to reach the final handler configurations.
541    PerCatConfig = maps:from_list(
542                     [{Cat, normalize_per_cat_log_config(Props)}
543                      || {Cat, Props} <- CatProps]),
544    GlobalConfig = normalize_main_log_config(EnvWithoutCats, DefaultProps),
545    #{global => GlobalConfig,
546      per_category => PerCatConfig}.
547
548-spec get_log_app_env() -> log_app_env().
549
550get_log_app_env() ->
551    application:get_env(rabbit, log, []).
552
553-spec normalize_main_log_config(main_log_env(), default_cat_env()) ->
554    global_log_config().
555
556normalize_main_log_config(Props, DefaultProps) ->
557    Outputs = case proplists:get_value(level, DefaultProps) of
558                  undefined -> #{outputs => []};
559                  Level     -> #{outputs => [],
560                                 level => Level}
561              end,
562    Props1 = compute_implicitly_enabled_output(Props),
563    normalize_main_log_config1(Props1, Outputs).
564
565compute_implicitly_enabled_output(Props) ->
566    {ConsoleEnabled, Props1} = compute_implicitly_enabled_output(
567                                 console, Props),
568    {ExchangeEnabled, Props2} = compute_implicitly_enabled_output(
569                                  exchange, Props1),
570    {JournaldEnabled, Props3} = compute_implicitly_enabled_output(
571                                  journald, Props2),
572    {SyslogEnabled, Props4} = compute_implicitly_enabled_output(
573                                syslog, Props3),
574    FileDisabledByDefault =
575    ConsoleEnabled orelse
576    ExchangeEnabled orelse
577    JournaldEnabled orelse
578    SyslogEnabled,
579
580    FileProps = proplists:get_value(file, Props4, []),
581    case is_output_explicitely_enabled(FileProps) of
582        true ->
583            Props4;
584        false ->
585            case FileDisabledByDefault of
586                true ->
587                    FileProps1 = lists:keystore(
588                                   file, 1, FileProps, {file, false}),
589                    lists:keystore(
590                      file, 1, Props4, {file, FileProps1});
591                false ->
592                    Props4
593            end
594    end.
595
596compute_implicitly_enabled_output(PropName, Props) ->
597    SubProps = proplists:get_value(PropName, Props, []),
598    {Enabled, SubProps1} = compute_implicitly_enabled_output1(SubProps),
599    {Enabled,
600     lists:keystore(PropName, 1, Props, {PropName, SubProps1})}.
601
602compute_implicitly_enabled_output1(SubProps) ->
603    %% We consider the output enabled or disabled if:
604    %%     * it is explicitely marked as such, or
605    %%     * the level is set to a log level (enabled) or `none' (disabled)
606    Enabled = proplists:get_value(
607                enabled, SubProps,
608                proplists:get_value(level, SubProps, none) =/= none),
609    {Enabled,
610     lists:keystore(enabled, 1, SubProps, {enabled, Enabled})}.
611
612is_output_explicitely_enabled(FileProps) ->
613    %% We consider the output enabled or disabled if:
614    %%     * the file is explicitely set, or
615    %%     * the level is set to a log level (enabled) or `none' (disabled)
616    File = proplists:get_value(file, FileProps),
617    Level = proplists:get_value(level, FileProps),
618    is_list(File) orelse (Level =/= undefined andalso Level =/= none).
619
620normalize_main_log_config1([{Type, Props} | Rest],
621                           #{outputs := Outputs} = LogConfig) ->
622    Outputs1 = normalize_main_output(Type, Props, Outputs),
623    LogConfig1 = LogConfig#{outputs => Outputs1},
624    normalize_main_log_config1(Rest, LogConfig1);
625normalize_main_log_config1([], LogConfig) ->
626    LogConfig.
627
628-spec normalize_main_output
629(console, console_props(), [logger:handler_config()]) ->
630    [logger:handler_config()];
631(exchange, exchange_props(), [logger:handler_config()]) ->
632    [logger:handler_config()];
633(file, file_props(), [logger:handler_config()]) ->
634    [logger:handler_config()];
635(journald, journald_props(), [logger:handler_config()]) ->
636    [logger:handler_config()];
637(syslog, syslog_props(), [logger:handler_config()]) ->
638    [logger:handler_config()].
639
640normalize_main_output(console, Props, Outputs) ->
641    normalize_main_console_output(
642      Props,
643      #{module => rabbit_logger_std_h,
644        config => #{type => standard_io}},
645      Outputs);
646normalize_main_output(exchange, Props, Outputs) ->
647    normalize_main_exchange_output(
648      Props,
649      #{module => rabbit_logger_exchange_h,
650        config => #{}},
651      Outputs);
652normalize_main_output(file, Props, Outputs) ->
653    normalize_main_file_output(
654      Props,
655      #{module => rabbit_logger_std_h,
656        config => #{type => file}},
657      Outputs);
658normalize_main_output(journald, Props, Outputs) ->
659    normalize_main_journald_output(
660      Props,
661      #{module => systemd_journal_h,
662        config => #{}},
663      Outputs);
664normalize_main_output(syslog, Props, Outputs) ->
665    normalize_main_syslog_output(
666      Props,
667      #{module => syslog_logger_h,
668        config => #{}},
669      Outputs).
670
671-spec normalize_main_file_output(file_props(), logger:handler_config(),
672                                 [logger:handler_config()]) ->
673    [logger:handler_config()].
674
675normalize_main_file_output(Props, Output, Outputs) ->
676    Enabled = case proplists:get_value(file, Props) of
677                  false     -> false;
678                  _         -> true
679              end,
680    case Enabled of
681        true  -> normalize_main_file_output1(Props, Output, Outputs);
682        false -> remove_main_file_output(Outputs)
683    end.
684
685normalize_main_file_output1(
686  [{file, Filename} | Rest],
687  #{config := Config} = Output, Outputs) ->
688    Output1 = Output#{config => Config#{file => Filename}},
689    normalize_main_file_output1(Rest, Output1, Outputs);
690normalize_main_file_output1(
691  [{level, Level} | Rest],
692  Output, Outputs) ->
693    Output1 = Output#{level => Level},
694    normalize_main_file_output1(Rest, Output1, Outputs);
695normalize_main_file_output1(
696  [{date, DateSpec} | Rest],
697  #{config := Config} = Output, Outputs) ->
698    Output1 = Output#{config => Config#{rotate_on_date => DateSpec}},
699    normalize_main_file_output1(Rest, Output1, Outputs);
700normalize_main_file_output1(
701  [{size, Size} | Rest],
702  #{config := Config} = Output, Outputs) ->
703    Output1 = Output#{config => Config#{max_no_bytes => Size}},
704    normalize_main_file_output1(Rest, Output1, Outputs);
705normalize_main_file_output1(
706  [{count, Count} | Rest],
707  #{config := Config} = Output, Outputs) ->
708    Output1 = Output#{config => Config#{max_no_files => Count}},
709    normalize_main_file_output1(Rest, Output1, Outputs);
710normalize_main_file_output1(
711  [{formatter, undefined} | Rest],
712  Output, Outputs) ->
713    normalize_main_file_output1(Rest, Output, Outputs);
714normalize_main_file_output1(
715  [{formatter, Formatter} | Rest],
716  Output, Outputs) ->
717    Output1 = Output#{formatter => Formatter},
718    normalize_main_file_output1(Rest, Output1, Outputs);
719normalize_main_file_output1([], Output, Outputs) ->
720    [Output | Outputs].
721
722remove_main_file_output(Outputs) ->
723    lists:filter(
724      fun
725          (#{module := rabbit_logger_std_h,
726             config := #{type := file}}) -> false;
727          (_)                            -> true
728      end, Outputs).
729
730-spec normalize_main_console_output(console_props(), logger:handler_config(),
731                                    [logger:handler_config()]) ->
732    [logger:handler_config()].
733
734normalize_main_console_output(Props, Output, Outputs) ->
735    Enabled = proplists:get_value(enabled, Props),
736    case Enabled of
737        true  -> normalize_main_console_output1(Props, Output, Outputs);
738        false -> remove_main_console_output(Output, Outputs)
739    end.
740
741normalize_main_console_output1(
742  [{enabled, true} | Rest],
743  Output, Outputs) ->
744    normalize_main_console_output1(Rest, Output, Outputs);
745normalize_main_console_output1(
746  [{level, Level} | Rest],
747  Output, Outputs) ->
748    Output1 = Output#{level => Level},
749    normalize_main_console_output1(Rest, Output1, Outputs);
750normalize_main_console_output1(
751  [{stdio, stdout} | Rest],
752  #{config := Config} = Output, Outputs) ->
753    Config1 = Config#{type => standard_io},
754    Output1 = Output#{config => Config1},
755    normalize_main_console_output1(Rest, Output1, Outputs);
756normalize_main_console_output1(
757  [{stdio, stderr} | Rest],
758  #{config := Config} = Output, Outputs) ->
759    Config1 = Config#{type => standard_error},
760    Output1 = Output#{config => Config1},
761    normalize_main_console_output1(Rest, Output1, Outputs);
762normalize_main_console_output1(
763  [{formatter, undefined} | Rest],
764  Output, Outputs) ->
765    normalize_main_console_output1(Rest, Output, Outputs);
766normalize_main_console_output1(
767  [{formatter, Formatter} | Rest],
768  Output, Outputs) ->
769    Output1 = Output#{formatter => Formatter},
770    normalize_main_console_output1(Rest, Output1, Outputs);
771normalize_main_console_output1([], Output, Outputs) ->
772    [Output | Outputs].
773
774remove_main_console_output(
775  #{module := Mod1, config := #{type := Stddev}},
776  Outputs)
777  when ?IS_STD_H_COMPAT(Mod1) andalso
778       ?IS_STDDEV(Stddev) ->
779    lists:filter(
780      fun
781          (#{module := Mod2,
782             config := #{type := standard_io}})
783            when ?IS_STD_H_COMPAT(Mod2) ->
784              false;
785          (#{module := Mod2,
786             config := #{type := standard_error}})
787            when ?IS_STD_H_COMPAT(Mod2) ->
788              false;
789          (_) ->
790              true
791      end, Outputs).
792
793-spec normalize_main_exchange_output(
794        exchange_props(), logger:handler_config(),
795        [logger:handler_config()]) ->
796    [logger:handler_config()].
797
798normalize_main_exchange_output(Props, Output, Outputs) ->
799    Enabled = proplists:get_value(enabled, Props),
800    case Enabled of
801        true  -> normalize_main_exchange_output1(Props, Output, Outputs);
802        false -> remove_main_exchange_output(Output, Outputs)
803    end.
804
805normalize_main_exchange_output1(
806  [{enabled, true} | Rest],
807  Output, Outputs) ->
808    normalize_main_exchange_output1(Rest, Output, Outputs);
809normalize_main_exchange_output1(
810  [{level, Level} | Rest],
811  Output, Outputs) ->
812    Output1 = Output#{level => Level},
813    normalize_main_exchange_output1(Rest, Output1, Outputs);
814normalize_main_exchange_output1(
815  [{formatter, undefined} | Rest],
816  Output, Outputs) ->
817    normalize_main_exchange_output1(Rest, Output, Outputs);
818normalize_main_exchange_output1(
819  [{formatter, Formatter} | Rest],
820  Output, Outputs) ->
821    Output1 = Output#{formatter => Formatter},
822    normalize_main_exchange_output1(Rest, Output1, Outputs);
823normalize_main_exchange_output1([], Output, Outputs) ->
824    [Output | Outputs].
825
826remove_main_exchange_output(
827  #{module := rabbit_logger_exchange_h}, Outputs) ->
828    lists:filter(
829      fun
830          (#{module := rabbit_logger_exchange_h}) -> false;
831          (_)                                     -> true
832      end, Outputs).
833
834-spec normalize_main_journald_output(journald_props(), logger:handler_config(),
835                                     [logger:handler_config()]) ->
836    [logger:handler_config()].
837
838normalize_main_journald_output(Props, Output, Outputs) ->
839    Enabled = proplists:get_value(enabled, Props),
840    case Enabled of
841        true  -> normalize_main_journald_output1(Props, Output, Outputs);
842        false -> remove_main_journald_output(Output, Outputs)
843    end.
844
845normalize_main_journald_output1(
846  [{enabled, true} | Rest],
847  Output, Outputs) ->
848    normalize_main_journald_output1(Rest, Output, Outputs);
849normalize_main_journald_output1(
850  [{level, Level} | Rest],
851  Output, Outputs) ->
852    Output1 = Output#{level => Level},
853    normalize_main_journald_output1(Rest, Output1, Outputs);
854normalize_main_journald_output1(
855  [{fields, FieldMapping} | Rest],
856  #{config := Config} = Output, Outputs) ->
857    Config1 = Config#{fields => FieldMapping},
858    Output1 = Output#{config => Config1},
859    normalize_main_journald_output1(Rest, Output1, Outputs);
860normalize_main_journald_output1(
861  [{formatter, undefined} | Rest],
862  Output, Outputs) ->
863    normalize_main_journald_output1(Rest, Output, Outputs);
864normalize_main_journald_output1(
865  [{formatter, Formatter} | Rest],
866  Output, Outputs) ->
867    Output1 = Output#{formatter => Formatter},
868    normalize_main_journald_output1(Rest, Output1, Outputs);
869normalize_main_journald_output1([], Output, Outputs) ->
870    [Output | Outputs].
871
872remove_main_journald_output(
873  #{module := systemd_journal_h},
874  Outputs) ->
875    lists:filter(
876      fun
877          (#{module := systemd_journal_h}) -> false;
878          (_)                              -> true
879      end, Outputs).
880
881-spec normalize_main_syslog_output(
882        syslog_props(), logger:handler_config(),
883        [logger:handler_config()]) ->
884    [logger:handler_config()].
885
886normalize_main_syslog_output(Props, Output, Outputs) ->
887    Enabled = proplists:get_value(enabled, Props),
888    case Enabled of
889        true  -> normalize_main_syslog_output1(Props, Output, Outputs);
890        false -> remove_main_syslog_output(Output, Outputs)
891    end.
892
893normalize_main_syslog_output1(
894  [{enabled, true} | Rest],
895  Output, Outputs) ->
896    normalize_main_syslog_output1(Rest, Output, Outputs);
897normalize_main_syslog_output1(
898  [{level, Level} | Rest],
899  Output, Outputs) ->
900    Output1 = Output#{level => Level},
901    normalize_main_syslog_output1(Rest, Output1, Outputs);
902normalize_main_syslog_output1(
903  [{formatter, undefined} | Rest],
904  Output, Outputs) ->
905    normalize_main_syslog_output1(Rest, Output, Outputs);
906normalize_main_syslog_output1(
907  [{formatter, Formatter} | Rest],
908  Output, Outputs) ->
909    Output1 = Output#{formatter => Formatter},
910    normalize_main_syslog_output1(Rest, Output1, Outputs);
911normalize_main_syslog_output1([], Output, Outputs) ->
912    [Output | Outputs].
913
914remove_main_syslog_output(
915  #{module := syslog_logger_h}, Outputs) ->
916    lists:filter(
917      fun
918          (#{module := syslog_logger_h}) -> false;
919          (_)                            -> true
920      end, Outputs).
921
922-spec normalize_per_cat_log_config(per_cat_env()) -> per_cat_log_config().
923
924normalize_per_cat_log_config(Props) ->
925    normalize_per_cat_log_config(Props, #{outputs => []}).
926
927normalize_per_cat_log_config([{level, Level} | Rest], LogConfig) ->
928    LogConfig1 = LogConfig#{level => Level},
929    normalize_per_cat_log_config(Rest, LogConfig1);
930normalize_per_cat_log_config([{file, Filename} | Rest],
931                             #{outputs := Outputs} = LogConfig) ->
932    %% Caution: The `file' property in the per-category configuration only
933    %% accepts a filename. It doesn't support the properties of the `file'
934    %% property at the global configuration level.
935    Output = #{module => rabbit_logger_std_h,
936               config => #{type => file,
937                           file => Filename}},
938    LogConfig1 = LogConfig#{outputs => [Output | Outputs]},
939    normalize_per_cat_log_config(Rest, LogConfig1);
940normalize_per_cat_log_config([], LogConfig) ->
941    LogConfig.
942
943-spec handle_default_and_overridden_outputs(log_config(),
944                                            rabbit_env:context()) ->
945    log_config().
946
947handle_default_and_overridden_outputs(LogConfig, Context) ->
948    LogConfig1 = handle_default_main_output(LogConfig, Context),
949    LogConfig2 = handle_default_upgrade_cat_output(LogConfig1, Context),
950    LogConfig2.
951
952-spec handle_default_main_output(log_config(), rabbit_env:context()) ->
953    log_config().
954
955handle_default_main_output(
956  #{global := #{outputs := Outputs} = GlobalConfig} = LogConfig,
957  #{main_log_file := MainLogFile} = Context) ->
958    NoOutputsConfigured = Outputs =:= [],
959    Overridden = rabbit_env:has_var_been_overridden(Context, main_log_file),
960    Outputs1 = if
961                   NoOutputsConfigured orelse Overridden ->
962                       Output0 = log_file_var_to_output(MainLogFile),
963                       Output1 = keep_log_level_from_equivalent_output(
964                                   Output0, Outputs),
965                       [Output1];
966                   true ->
967                       [case Output of
968                            #{module := Mod,
969                              config := #{type := file, file := _}}
970                              when ?IS_STD_H_COMPAT(Mod) ->
971                                Output;
972                            #{module := Mod,
973                              config := #{type := file} = Config}
974                              when ?IS_STD_H_COMPAT(Mod) ->
975                                Output#{config =>
976                                        Config#{file => MainLogFile}};
977                            _ ->
978                                Output
979                        end || Output <- Outputs]
980               end,
981    case Outputs1 of
982        Outputs -> LogConfig;
983        _       -> LogConfig#{
984                     global => GlobalConfig#{
985                                 outputs => Outputs1}}
986    end.
987
988-spec handle_default_upgrade_cat_output(log_config(), rabbit_env:context()) ->
989    log_config().
990
991handle_default_upgrade_cat_output(
992  #{per_category := PerCatConfig} = LogConfig,
993  #{upgrade_log_file := UpgLogFile} = Context) ->
994    UpgCatConfig = case PerCatConfig of
995                       #{upgrade := CatConfig} -> CatConfig;
996                       _                       -> #{outputs => []}
997                   end,
998    #{outputs := Outputs} = UpgCatConfig,
999    NoOutputsConfigured = Outputs =:= [],
1000    Overridden = rabbit_env:has_var_been_overridden(
1001                   Context, upgrade_log_file),
1002    Outputs1 = if
1003                   NoOutputsConfigured orelse Overridden ->
1004                       Output0 = log_file_var_to_output(UpgLogFile),
1005                       Output1 = keep_log_level_from_equivalent_output(
1006                                   Output0, Outputs),
1007                       [Output1];
1008                   true ->
1009                      Outputs
1010               end,
1011    case Outputs1 of
1012        Outputs -> LogConfig;
1013        _       -> LogConfig#{
1014                     per_category => PerCatConfig#{
1015                                       upgrade => UpgCatConfig#{
1016                                                    outputs => Outputs1}}}
1017    end.
1018
1019-spec log_file_var_to_output(file:filename() | string()) ->
1020    logger:handler_config().
1021
1022log_file_var_to_output("-") ->
1023    #{module => rabbit_logger_std_h,
1024      config => #{type => standard_io}};
1025log_file_var_to_output("-stderr") ->
1026    #{module => rabbit_logger_std_h,
1027      config => #{type => standard_error}};
1028log_file_var_to_output("exchange:" ++ _) ->
1029    #{module => rabbit_logger_exchange_h,
1030      config => #{}};
1031log_file_var_to_output("journald:" ++ _) ->
1032    #{module => systemd_journal_h,
1033      config => #{}};
1034log_file_var_to_output("syslog:" ++ _) ->
1035    #{module => syslog_logger_h,
1036      config => #{}};
1037log_file_var_to_output(Filename) ->
1038    #{module => rabbit_logger_std_h,
1039      config => #{type => file,
1040                  file => Filename}}.
1041
1042-spec keep_log_level_from_equivalent_output(
1043        logger:handler_config(), [logger:handler_config()]) ->
1044    logger:handler_config().
1045%% @doc
1046%% Keeps the log level from the equivalent output if found in the given list of
1047%% outputs.
1048%%
1049%% If the output is overridden from the environment, or if no output is
1050%% configured at all (and the default output is used), we should still keep the
1051%% log level set in the configuration. The idea is that the $RABBITMQ_LOGS
1052%% environment variable only overrides the output, not its log level (which
1053%% would be set in $RABBITMQ_LOG).
1054%%
1055%% Here is an example of when it is used:
1056%% * "$RABBITMQ_LOGS=-" is set in the environment
1057%% * "log.console.level = debug" is set in the configuration file
1058
1059keep_log_level_from_equivalent_output(
1060  #{module := Mod, config := #{type := Type}} = Output,
1061  [#{module := Mod, config := #{type := Type}} = OverridenOutput | _])
1062  when ?IS_STD_H_COMPAT(Mod) ->
1063    keep_log_level_from_equivalent_output1(Output, OverridenOutput);
1064keep_log_level_from_equivalent_output(
1065  #{module := Mod} = Output,
1066  [#{module := Mod} = OverridenOutput | _]) ->
1067    keep_log_level_from_equivalent_output1(Output, OverridenOutput);
1068keep_log_level_from_equivalent_output(Output, [_ | Rest]) ->
1069    keep_log_level_from_equivalent_output(Output, Rest);
1070keep_log_level_from_equivalent_output(Output, []) ->
1071    Output.
1072
1073-spec keep_log_level_from_equivalent_output1(
1074        logger:handler_config(), logger:handler_config()) ->
1075    logger:handler_config().
1076
1077keep_log_level_from_equivalent_output1(Output, #{level := Level}) ->
1078    Output#{level => Level};
1079keep_log_level_from_equivalent_output1(Output, _) ->
1080    Output.
1081
1082-spec apply_log_levels_from_env(log_config(), rabbit_env:context()) ->
1083    log_config().
1084
1085apply_log_levels_from_env(LogConfig, #{log_levels := LogLevels})
1086  when is_map(LogLevels) ->
1087    %% `LogLevels' comes from the `$RABBITMQ_LOG' environment variable. It has
1088    %% the following form:
1089    %%     RABBITMQ_LOG=$cat1=$level1,$cat2=$level2,+color,-json
1090    %% I.e. it contains either `category=level' or `+flag'/`-flag'.
1091    %%
1092    %% Here we want to apply the log levels set from that variable, but we
1093    %% need to filter out any flags.
1094    maps:fold(
1095      fun
1096          (_, Value, LC) when is_boolean(Value) ->
1097              %% Ignore the '+color' and '+json' flags.
1098              LC;
1099          (global, Level, #{global := GlobalConfig} = LC) ->
1100              GlobalConfig1 = GlobalConfig#{level => Level},
1101              LC#{global => GlobalConfig1};
1102          (CatString, Level, #{per_category := PerCatConfig} = LC) ->
1103              CatAtom = list_to_atom(CatString),
1104              CatConfig0 = maps:get(CatAtom, PerCatConfig, #{outputs => []}),
1105              CatConfig1 = CatConfig0#{level => Level},
1106              PerCatConfig1 = PerCatConfig#{CatAtom => CatConfig1},
1107              LC#{per_category => PerCatConfig1}
1108      end, LogConfig, LogLevels);
1109apply_log_levels_from_env(LogConfig, _) ->
1110    LogConfig.
1111
1112-spec make_filenames_absolute(log_config(), rabbit_env:context()) ->
1113    log_config().
1114
1115make_filenames_absolute(
1116  #{global := GlobalConfig, per_category := PerCatConfig} = LogConfig,
1117  Context) ->
1118    LogBaseDir = get_log_base_dir(Context),
1119    GlobalConfig1 = make_filenames_absolute1(GlobalConfig, LogBaseDir),
1120    PerCatConfig1 = maps:map(
1121                      fun(_, CatConfig) ->
1122                              make_filenames_absolute1(CatConfig, LogBaseDir)
1123                      end, PerCatConfig),
1124    LogConfig#{global => GlobalConfig1, per_category => PerCatConfig1}.
1125
1126make_filenames_absolute1(#{outputs := Outputs} = Config, LogBaseDir) ->
1127    Outputs1 = lists:map(
1128                 fun
1129                     (#{module := Mod,
1130                        config := #{type := file,
1131                                    file := Filename} = Cfg} = Output)
1132                       when ?IS_STD_H_COMPAT(Mod) ->
1133                         Cfg1 = Cfg#{file => filename:absname(
1134                                               Filename, LogBaseDir)},
1135                         Output#{config => Cfg1};
1136                     (Output) ->
1137                         Output
1138                 end, Outputs),
1139    Config#{outputs => Outputs1}.
1140
1141-spec configure_formatters(log_config(), rabbit_env:context()) ->
1142    log_config().
1143
1144configure_formatters(
1145  #{global := GlobalConfig, per_category := PerCatConfig} = LogConfig,
1146  Context) ->
1147    GlobalConfig1 = configure_formatters1(GlobalConfig, Context),
1148    PerCatConfig1 = maps:map(
1149                      fun(_, CatConfig) ->
1150                              configure_formatters1(CatConfig, Context)
1151                      end, PerCatConfig),
1152    LogConfig#{global => GlobalConfig1, per_category => PerCatConfig1}.
1153
1154configure_formatters1(#{outputs := Outputs} = Config, Context) ->
1155    %% TODO: Add ability to configure formatters from the Cuttlefish
1156    %% configuration file. For now, it is only possible from the
1157    %% `$RABBITMQ_LOG' environment variable.
1158    ConsFormatter =
1159    rabbit_prelaunch_early_logging:default_console_formatter(Context),
1160    FileFormatter =
1161    rabbit_prelaunch_early_logging:default_file_formatter(Context),
1162    JournaldFormatter =
1163    rabbit_prelaunch_early_logging:default_journald_formatter(Context),
1164    SyslogFormatter =
1165    rabbit_prelaunch_early_logging:default_syslog_formatter(Context),
1166    Outputs1 = lists:map(
1167                 fun
1168                     (#{module := Mod,
1169                        config := #{type := Stddev}} = Output)
1170                       when ?IS_STD_H_COMPAT(Mod) andalso
1171                            ?IS_STDDEV(Stddev) ->
1172                         case maps:is_key(formatter, Output) of
1173                             true  -> Output;
1174                             false -> Output#{formatter => ConsFormatter}
1175                         end;
1176                     (#{module := systemd_journal_h} = Output) ->
1177                         case maps:is_key(formatter, Output) of
1178                             true  -> Output;
1179                             false -> Output#{formatter => JournaldFormatter}
1180                         end;
1181                     (#{module := syslog_logger_h} = Output) ->
1182                         case maps:is_key(formatter, Output) of
1183                             true  -> Output;
1184                             false -> Output#{formatter => SyslogFormatter}
1185                         end;
1186                     (Output) ->
1187                         case maps:is_key(formatter, Output) of
1188                             true  -> Output;
1189                             false -> Output#{formatter => FileFormatter}
1190                         end
1191                 end, Outputs),
1192    Config#{outputs => Outputs1}.
1193
1194-spec create_logger_handlers_conf(log_config()) ->
1195    [logger:handler_config()].
1196
1197create_logger_handlers_conf(
1198  #{global := GlobalConfig, per_category := PerCatConfig}) ->
1199    Handlers0 = create_global_handlers_conf(GlobalConfig),
1200    Handlers1 = create_per_cat_handlers_conf(PerCatConfig, Handlers0),
1201    Handlers2 = adjust_log_levels(Handlers1),
1202
1203    %% assign_handler_ids/1 is also responsible for transforming the map of
1204    %% handlers into a list. The map was only used to deduplicate handlers.
1205    assign_handler_ids(Handlers2).
1206
1207-spec create_global_handlers_conf(global_log_config()) ->
1208    #{handler_key() := logger:handler_config()}.
1209
1210create_global_handlers_conf(#{outputs := Outputs} = GlobalConfig) ->
1211    Handlers = ensure_handlers_conf(Outputs, global, GlobalConfig, #{}),
1212    maps:map(
1213      fun(_, Handler) ->
1214              add_erlang_specific_filters(Handler)
1215      end, Handlers).
1216
1217-spec add_erlang_specific_filters(logger:handler_config()) ->
1218    logger:handler_config().
1219
1220add_erlang_specific_filters(#{filters := Filters} = Handler) ->
1221    %% We only log progress reports (from application master and supervisor)
1222    %% only if the handler level is set to debug.
1223    Action = case Handler of
1224                 #{level := debug} -> log;
1225                 _                 -> stop
1226             end,
1227    Filters1 = [{progress_reports, {fun logger_filters:progress/2, Action}}
1228                | Filters],
1229    Handler#{filters => Filters1}.
1230
1231-spec create_per_cat_handlers_conf(
1232        #{category_name() => per_cat_log_config()},
1233        #{handler_key() => logger:handler_config()}) ->
1234    #{handler_key() => logger:handler_config()}.
1235
1236create_per_cat_handlers_conf(PerCatConfig, Handlers) ->
1237    maps:fold(
1238      fun
1239          (CatName, #{outputs := []} = CatConfig, Hdls) ->
1240              %% That category has no outputs defined. It means its messages
1241              %% will go to the global handlers. We still need to update
1242              %% global handlers to filter the level of the messages from that
1243              %% category.
1244              filter_cat_in_global_handlers(Hdls, CatName, CatConfig);
1245          (CatName, #{outputs := Outputs} = CatConfig, Hdls) ->
1246              %% That category has specific outputs (i.e. in addition to the
1247              %% global handlers).
1248              Hdls1 = ensure_handlers_conf(Outputs, CatName, CatConfig, Hdls),
1249              %% We need to filter the messages from that category out in the
1250              %% global handlers.
1251              filter_out_cat_in_global_handlers(Hdls1, CatName)
1252      end, Handlers, PerCatConfig).
1253
1254-spec ensure_handlers_conf(
1255        [logger:handler_config()], global | category_name(),
1256        global_log_config() | per_cat_log_config(),
1257        #{handler_key() => logger:handler_config()}) ->
1258    #{handler_key() => logger:handler_config()}.
1259
1260ensure_handlers_conf([Output | Rest], CatName, Config, Handlers) ->
1261    Key = create_handler_key(Output),
1262    %% This is where the deduplication happens: either we update the existing
1263    %% handler (based on the key computed above) or we create a new one.
1264    Handler = case maps:is_key(Key, Handlers) of
1265                  false -> create_handler_conf(Output, CatName, Config);
1266                  true  -> update_handler_conf(maps:get(Key, Handlers),
1267                                               CatName, Output)
1268              end,
1269    Handlers1 = Handlers#{Key => Handler},
1270    ensure_handlers_conf(Rest, CatName, Config, Handlers1);
1271ensure_handlers_conf([], _, _, Handlers) ->
1272    Handlers.
1273
1274-spec create_handler_key(logger:handler_config()) -> handler_key().
1275
1276create_handler_key(
1277  #{module := Mod, config := #{type := file, file := Filename}})
1278  when ?IS_STD_H_COMPAT(Mod) ->
1279    {file, Filename};
1280create_handler_key(
1281  #{module := Mod, config := #{type := standard_io}})
1282  when ?IS_STD_H_COMPAT(Mod) ->
1283    {console, standard_io};
1284create_handler_key(
1285  #{module := Mod, config := #{type := standard_error}})
1286  when ?IS_STD_H_COMPAT(Mod) ->
1287    {console, standard_error};
1288create_handler_key(
1289  #{module := rabbit_logger_exchange_h}) ->
1290    exchange;
1291create_handler_key(
1292  #{module := systemd_journal_h}) ->
1293    journald;
1294create_handler_key(
1295  #{module := syslog_logger_h}) ->
1296    syslog.
1297
1298-spec create_handler_conf(
1299        logger:handler_config(), global | category_name(),
1300        global_log_config() | per_cat_log_config()) ->
1301    logger:handler_config().
1302
1303%% The difference between a global handler and a category handler is the value
1304%% of `filter_default'. In a global hanler, if a message was not stopped or
1305%% explicitely accepted by a filter, the message is logged. In a category
1306%% handler, it is dropped.
1307
1308create_handler_conf(Output, global, Config) ->
1309    Level = compute_level_from_config_and_output(Config, Output),
1310    Output#{level => Level,
1311            filter_default => log,
1312            filters => [{?FILTER_NAME,
1313                         {fun filter_log_event/2, #{global => Level}}}]};
1314create_handler_conf(Output, CatName, Config) ->
1315    Level = compute_level_from_config_and_output(Config, Output),
1316    Output#{level => Level,
1317            filter_default => stop,
1318            filters => [{?FILTER_NAME,
1319                         {fun filter_log_event/2, #{CatName => Level}}}]}.
1320
1321-spec update_handler_conf(
1322        logger:handler_config(), global | category_name(),
1323        logger:handler_config()) ->
1324    logger:handler_config().
1325
1326update_handler_conf(
1327  #{level := ConfiguredLevel} = Handler, global, Output) ->
1328    case Output of
1329        #{level := NewLevel} ->
1330            Handler#{level =>
1331                     get_less_severe_level(NewLevel, ConfiguredLevel)};
1332        _ ->
1333            Handler
1334    end;
1335update_handler_conf(Handler, CatName, Output) ->
1336    add_cat_filter(Handler, CatName, Output).
1337
1338-spec compute_level_from_config_and_output(
1339        global_log_config() | per_cat_log_config(),
1340        logger:handler_config()) ->
1341    logger:level().
1342%% @doc
1343%% Compute the debug level for a handler.
1344%%
1345%% The precedence is:
1346%% <ol>
1347%% <li>the level of the output</li>
1348%% <li>the level of the category (or the global level)</li>
1349%% <li>the default value</li>
1350%% </ol>
1351
1352compute_level_from_config_and_output(Config, Output) ->
1353    case Output of
1354        #{level := Level} ->
1355            Level;
1356        _ ->
1357            case Config of
1358                #{level := Level} -> Level;
1359                _                 -> ?DEFAULT_LOG_LEVEL
1360            end
1361    end.
1362
1363-spec filter_cat_in_global_handlers(
1364        #{handler_key() => logger:handler_config()}, category_name(),
1365        per_cat_log_config()) ->
1366    #{handler_key() => logger:handler_config()}.
1367
1368filter_cat_in_global_handlers(Handlers, CatName, CatConfig) ->
1369    maps:map(
1370      fun
1371          (_, #{filter_default := log} = Handler) ->
1372              add_cat_filter(Handler, CatName, CatConfig);
1373          (_, Handler) ->
1374              Handler
1375      end, Handlers).
1376
1377-spec filter_out_cat_in_global_handlers(
1378        #{handler_key() => logger:handler_config()}, category_name()) ->
1379    #{handler_key() => logger:handler_config()}.
1380
1381filter_out_cat_in_global_handlers(Handlers, CatName) ->
1382    maps:map(
1383      fun
1384          (_, #{filter_default := log, filters := Filters} = Handler) ->
1385              {_, FilterConfig} = proplists:get_value(?FILTER_NAME, Filters),
1386              case maps:is_key(CatName, FilterConfig) of
1387                  true  -> Handler;
1388                  false -> add_cat_filter(Handler, CatName, #{level => none,
1389                                                              outputs => []})
1390              end;
1391          (_, Handler) ->
1392              Handler
1393      end, Handlers).
1394
1395-spec add_cat_filter(
1396        logger:handler_config(), category_name(),
1397        per_cat_log_config() | logger:handler_config()) ->
1398    logger:handler_config().
1399
1400add_cat_filter(Handler, CatName, CatConfigOrOutput) ->
1401    Level = case CatConfigOrOutput of
1402                #{level := L} -> L;
1403                _             -> maps:get(level, Handler)
1404            end,
1405    do_add_cat_filter(Handler, CatName, Level).
1406
1407do_add_cat_filter(#{filters := Filters} = Handler, CatName, Level) ->
1408    {Fun, FilterConfig} = proplists:get_value(?FILTER_NAME, Filters),
1409    FilterConfig1 = FilterConfig#{CatName => Level},
1410    Filters1 = lists:keystore(?FILTER_NAME, 1, Filters,
1411                              {?FILTER_NAME, {Fun, FilterConfig1}}),
1412    Handler#{filters => Filters1}.
1413
1414-spec filter_log_event(logger:log_event(), term()) -> logger:filter_return().
1415
1416filter_log_event(LogEvent, FilterConfig) ->
1417    rabbit_prelaunch_early_logging:filter_log_event(LogEvent, FilterConfig).
1418
1419-spec adjust_log_levels(#{handler_key() => logger:handler_config()}) ->
1420    #{handler_key() => logger:handler_config()}.
1421%% @doc
1422%% Adjust handler log level based on the filters' level.
1423%%
1424%% If a filter is more permissive, we need to adapt the handler log level so
1425%% the message makes it to the filter.
1426%%
1427%% Also, if the log level is set to `debug', we turn off burst limit to make
1428%% sure all debug messages make it.
1429
1430adjust_log_levels(Handlers) ->
1431    maps:map(
1432      fun(_, #{level := GeneralLevel, filters := Filters} = Handler) ->
1433              {_, FilterConfig} = proplists:get_value(?FILTER_NAME, Filters),
1434              Level = maps:fold(
1435                        fun(_, LvlA, LvlB) ->
1436                                get_less_severe_level(LvlA, LvlB)
1437                        end, GeneralLevel, FilterConfig),
1438              Handler1 = Handler#{level => Level},
1439              adjust_burst_limit(Handler1)
1440      end, Handlers).
1441
1442adjust_burst_limit(#{config := #{burst_limit_enable := _}} = Handler) ->
1443    Handler;
1444adjust_burst_limit(#{level := debug, config := Config} = Handler) ->
1445    Config1 = Config#{burst_limit_enable => false},
1446    Handler#{config => Config1};
1447adjust_burst_limit(Handler) when is_map(Handler) ->
1448    Handler.
1449
1450adjust_burst_limit(Config, Level) ->
1451    Config#{burst_limit_enable => Level =/= debug}.
1452
1453-spec assign_handler_ids(#{handler_key() => logger:handler_config()}) ->
1454    [logger:handler_config()].
1455
1456assign_handler_ids(Handlers) ->
1457    Handlers1 = [maps:get(Key, Handlers)
1458                 || Key <- lists:sort(maps:keys(Handlers))],
1459    assign_handler_ids(Handlers1,
1460                       #{config_run_number => get_config_run_number(),
1461                         next_file => 1},
1462                       []).
1463
1464-spec assign_handler_ids(
1465        [logger:handler_config()], id_assignment_state(),
1466        [logger:handler_config()]) ->
1467    [logger:handler_config()].
1468
1469assign_handler_ids(
1470  [#{module := Mod, config := #{type := file}} = Handler | Rest],
1471  #{next_file := NextFile} = State,
1472  Result)
1473  when ?IS_STD_H_COMPAT(Mod) ->
1474    Id = format_id("file_~b", [NextFile], State),
1475    Handler1 = Handler#{id => Id},
1476    assign_handler_ids(
1477      Rest, State#{next_file => NextFile + 1}, [Handler1 | Result]);
1478assign_handler_ids(
1479  [#{module := Mod, config := #{type := standard_io}} = Handler | Rest],
1480  State,
1481  Result)
1482  when ?IS_STD_H_COMPAT(Mod) ->
1483    Id = format_id("stdout", [], State),
1484    Handler1 = Handler#{id => Id},
1485    assign_handler_ids(Rest, State, [Handler1 | Result]);
1486assign_handler_ids(
1487  [#{module := Mod, config := #{type := standard_error}} = Handler | Rest],
1488  State,
1489  Result)
1490  when ?IS_STD_H_COMPAT(Mod) ->
1491    Id = format_id("stderr", [], State),
1492    Handler1 = Handler#{id => Id},
1493    assign_handler_ids(Rest, State, [Handler1 | Result]);
1494assign_handler_ids(
1495  [#{module := rabbit_logger_exchange_h} = Handler
1496   | Rest],
1497  State,
1498  Result) ->
1499    Id = format_id("exchange", [], State),
1500    Handler1 = Handler#{id => Id},
1501    assign_handler_ids(Rest, State, [Handler1 | Result]);
1502assign_handler_ids(
1503  [#{module := systemd_journal_h} = Handler
1504   | Rest],
1505  State,
1506  Result) ->
1507    Id = format_id("journald", [], State),
1508    Handler1 = Handler#{id => Id},
1509    assign_handler_ids(Rest, State, [Handler1 | Result]);
1510assign_handler_ids(
1511  [#{module := syslog_logger_h} = Handler
1512   | Rest],
1513  State,
1514  Result) ->
1515    Id = format_id("syslog", [], State),
1516    Handler1 = Handler#{id => Id},
1517    assign_handler_ids(Rest, State, [Handler1 | Result]);
1518assign_handler_ids([], _, Result) ->
1519    lists:reverse(Result).
1520
1521-spec format_id(io:format(), [term()], id_assignment_state()) ->
1522    logger:handler_id().
1523
1524format_id(Format, Args, #{config_run_number := RunNum}) ->
1525    list_to_atom(rabbit_misc:format("rmq_~b_" ++ Format, [RunNum | Args])).
1526
1527-spec install_handlers([logger:handler_config()]) -> ok | no_return().
1528
1529install_handlers([]) ->
1530    ok;
1531install_handlers(Handlers) ->
1532    case adjust_running_dependencies(Handlers) of
1533        ok ->
1534            ?LOG_NOTICE(
1535               "Logging: switching to configured handler(s); following "
1536               "messages may not be visible in this log output",
1537               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1538            ok = do_install_handlers(Handlers),
1539            ok = remove_old_handlers(),
1540            ok = define_primary_level(Handlers),
1541            ?LOG_NOTICE(
1542               "Logging: configured log handlers are now ACTIVE",
1543               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH});
1544        _ ->
1545            ?LOG_NOTICE(
1546               "Logging: failed to configure log handlers; keeping existing "
1547               "handlers",
1548               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1549            ok
1550    end.
1551
1552-spec adjust_running_dependencies([logger:handler_config()]) -> ok | error.
1553
1554adjust_running_dependencies(Handlers) ->
1555    %% Based on the log handlers' module, we determine the list of applications
1556    %% they depend on.
1557    %%
1558    %% The DefaultDeps lists all possible dependencies and marked them as
1559    %% unneeded. Then, if we have a log handler which depends on one of them,
1560    %% it is marked as needed. This way, we know what needs to be started AND
1561    %% stopped.
1562    %%
1563    %% DefaultDeps is of the form `#{ApplicationName => Needed}'.
1564    DefaultDeps = #{syslog => false},
1565    Deps = lists:foldl(
1566             fun
1567                 (#{module := syslog_logger_h}, Acc) -> Acc#{syslog => true};
1568                 (_, Acc)                            -> Acc
1569             end, DefaultDeps, Handlers),
1570    adjust_running_dependencies1(maps:to_list(Deps)).
1571
1572-spec adjust_running_dependencies1([{atom(), boolean()}]) -> ok | error.
1573
1574adjust_running_dependencies1([{App, true} | Rest]) ->
1575    ?LOG_DEBUG(
1576       "Logging: ensure log handler dependency '~s' is started", [App],
1577       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1578    case application:ensure_all_started(App) of
1579        {ok, _} ->
1580            adjust_running_dependencies1(Rest);
1581        {error, Reason} ->
1582            ?LOG_ERROR(
1583               "Failed to start log handlers dependency '~s': ~p",
1584               [App, Reason],
1585               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1586            error
1587    end;
1588adjust_running_dependencies1([{App, false} | Rest]) ->
1589    ?LOG_DEBUG(
1590       "Logging: ensure log handler dependency '~s' is stopped", [App],
1591       #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1592    case application:stop(App) of
1593        ok ->
1594            ok;
1595        {error, Reason} ->
1596            ?LOG_NOTICE(
1597               "Logging: failed to stop log handlers dependency '~s': ~p",
1598               [App, Reason],
1599               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH})
1600    end,
1601    adjust_running_dependencies1(Rest);
1602adjust_running_dependencies1([]) ->
1603    ok.
1604
1605-spec do_install_handlers([logger:handler_config()]) -> ok | no_return().
1606
1607do_install_handlers([#{id := Id, module := Module} = Handler | Rest]) ->
1608    case logger:add_handler(Id, Module, Handler) of
1609        ok ->
1610            ok = remove_syslog_logger_h_hardcoded_filters(Handler),
1611            do_install_handlers(Rest);
1612        {error, {handler_not_added, {open_failed, Filename, Reason}}} ->
1613            throw({error, {cannot_log_to_file, Filename, Reason}});
1614        {error, {handler_not_added, Reason}} ->
1615            throw({error, {cannot_log_to_file, unknown, Reason}})
1616    end;
1617do_install_handlers([]) ->
1618    ok.
1619
1620remove_syslog_logger_h_hardcoded_filters(
1621  #{id := Id, module := syslog_logger_h}) ->
1622    _ = logger:remove_handler_filter(Id, progress),
1623    _ = logger:remove_handler_filter(Id, remote_gl),
1624    ok;
1625remove_syslog_logger_h_hardcoded_filters(_) ->
1626    ok.
1627
1628-spec remove_old_handlers() -> ok.
1629
1630remove_old_handlers() ->
1631    _ = logger:remove_handler(default),
1632    RunNum = get_config_run_number(),
1633    lists:foreach(
1634      fun(Id) ->
1635              Ret = re:run(atom_to_list(Id), "^rmq_([0-9]+)_",
1636                           [{capture, all_but_first, list}]),
1637              case Ret of
1638                  {match, [NumStr]} ->
1639                      Num = erlang:list_to_integer(NumStr),
1640                      if
1641                          Num < RunNum ->
1642                              ?LOG_DEBUG(
1643                                "Logging: removing old logger handler ~s",
1644                                [Id],
1645                                #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1646                              ok = logger:remove_handler(Id);
1647                          true ->
1648                              ok
1649                      end;
1650                  _ ->
1651                      ok
1652              end
1653      end, lists:sort(logger:get_handler_ids())),
1654    ok.
1655
1656-spec define_primary_level([logger:handler_config()]) ->
1657    ok | {error, term()}.
1658
1659define_primary_level(Handlers) ->
1660    define_primary_level(Handlers, emergency).
1661
1662-spec define_primary_level([logger:handler_config()], logger:level()) ->
1663    ok | {error, term()}.
1664
1665define_primary_level([#{level := Level} | Rest], PrimaryLevel) ->
1666    NewLevel = get_less_severe_level(Level, PrimaryLevel),
1667    define_primary_level(Rest, NewLevel);
1668define_primary_level([], PrimaryLevel) ->
1669    logger:set_primary_config(level, PrimaryLevel).
1670
1671-spec get_less_severe_level(logger:level(), logger:level()) -> logger:level().
1672%% @doc
1673%% Compares two log levels and returns the less severe one.
1674%%
1675%% @param LevelA the log level to compare to LevelB.
1676%% @param LevelB the log level to compare to LevelA.
1677%%
1678%% @returns the less severe log level.
1679
1680get_less_severe_level(LevelA, LevelB) ->
1681    case logger:compare_levels(LevelA, LevelB) of
1682        lt -> LevelA;
1683        _  -> LevelB
1684    end.
1685
1686-spec maybe_log_test_messages(log_config()) -> ok.
1687
1688maybe_log_test_messages(
1689  #{per_category := #{prelaunch := #{level := debug}}}) ->
1690    log_test_messages();
1691maybe_log_test_messages(
1692  #{global := #{level := debug}}) ->
1693    log_test_messages();
1694maybe_log_test_messages(_) ->
1695    ok.
1696
1697-spec log_test_messages() -> ok.
1698
1699log_test_messages() ->
1700    ?LOG_DEBUG("Logging: testing debug log level",
1701               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1702    ?LOG_INFO("Logging: testing info log level",
1703              #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1704    ?LOG_NOTICE("Logging: testing notice log level",
1705                #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1706    ?LOG_WARNING("Logging: testing warning log level",
1707                 #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1708    ?LOG_ERROR("Logging: testing error log level",
1709               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1710    ?LOG_CRITICAL("Logging: testing critical log level",
1711                  #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1712    ?LOG_ALERT("Logging: testing alert log level",
1713               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
1714    ?LOG_EMERGENCY("Logging: testing emergency log level",
1715                   #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}).
1716