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) 2021 VMware, Inc. or its affiliates.  All rights reserved.
6%%
7
8-module(rabbit_logger_json_fmt).
9
10-export([format/2]).
11
12format(
13  #{msg := Msg,
14    level := Level,
15    meta := Meta},
16  Config) ->
17    FormattedLevel = unicode:characters_to_binary(
18                       rabbit_logger_fmt_helpers:format_level(Level, Config)),
19    FormattedMeta = format_meta(Meta, Config),
20    %% We need to call `unicode:characters_to_binary()' here and several other
21    %% places because `jsx:encode()' will format a string as a list of
22    %% integers (we don't blame it for that, it makes sense).
23    FormattedMsg = unicode:characters_to_binary(
24                     rabbit_logger_fmt_helpers:format_msg(Msg, Meta, Config)),
25    InitialDoc0 = FormattedMeta#{level => FormattedLevel,
26                                 msg => FormattedMsg},
27    InitialDoc = case level_to_verbosity(Level, Config) of
28                     undefined -> InitialDoc0;
29                     Verbosity -> InitialDoc0#{verbosity => Verbosity}
30                 end,
31    DocAfterMapping = apply_mapping_and_ordering(InitialDoc, Config),
32    Json = jsx:encode(DocAfterMapping),
33    [Json, $\n].
34
35level_to_verbosity(Level, #{verbosity_map := Mapping}) ->
36    case maps:is_key(Level, Mapping) of
37        true  -> maps:get(Level, Mapping);
38        false -> undefined
39    end;
40level_to_verbosity(_, _) ->
41    undefined.
42
43format_meta(Meta, Config) ->
44    maps:fold(
45      fun
46          (time, Timestamp, Acc) ->
47              FormattedTime0 = rabbit_logger_fmt_helpers:format_time(
48                                 Timestamp, Config),
49              FormattedTime1 = case is_number(FormattedTime0) of
50                                   true  -> FormattedTime0;
51                                   false -> unicode:characters_to_binary(
52                                              FormattedTime0)
53                               end,
54              Acc#{time => FormattedTime1};
55          (domain = Key, Components, Acc) ->
56              Term = unicode:characters_to_binary(
57                       string:join(
58                         [atom_to_list(Cmp) || Cmp <- Components],
59                         ".")),
60              Acc#{Key => Term};
61          (Key, Value, Acc) ->
62              case convert_to_types_accepted_by_jsx(Value) of
63                  false -> Acc;
64                  Term  -> Acc#{Key => Term}
65              end
66      end, #{}, Meta).
67
68convert_to_types_accepted_by_jsx(Term) when is_map(Term) ->
69    maps:map(
70      fun(_, Value) -> convert_to_types_accepted_by_jsx(Value) end,
71      Term);
72convert_to_types_accepted_by_jsx(Term) when is_list(Term) ->
73    case io_lib:deep_char_list(Term) of
74        true ->
75            unicode:characters_to_binary(Term);
76        false ->
77            [convert_to_types_accepted_by_jsx(E) || E <- Term]
78    end;
79convert_to_types_accepted_by_jsx(Term) when is_tuple(Term) ->
80    convert_to_types_accepted_by_jsx(erlang:tuple_to_list(Term));
81convert_to_types_accepted_by_jsx(Term) when is_function(Term) ->
82    String = erlang:fun_to_list(Term),
83    unicode:characters_to_binary(String);
84convert_to_types_accepted_by_jsx(Term) when is_pid(Term) ->
85    String = erlang:pid_to_list(Term),
86    unicode:characters_to_binary(String);
87convert_to_types_accepted_by_jsx(Term) when is_port(Term) ->
88    String = erlang:port_to_list(Term),
89    unicode:characters_to_binary(String);
90convert_to_types_accepted_by_jsx(Term) when is_reference(Term) ->
91    String = erlang:ref_to_list(Term),
92    unicode:characters_to_binary(String);
93convert_to_types_accepted_by_jsx(Term) ->
94    Term.
95
96apply_mapping_and_ordering(Doc, #{field_map := Mapping}) ->
97    apply_mapping_and_ordering(Mapping, Doc, []);
98apply_mapping_and_ordering(Doc, _) ->
99    maps:to_list(Doc).
100
101apply_mapping_and_ordering([{'$REST', false} | Rest], _, Result) ->
102    apply_mapping_and_ordering(Rest, #{}, Result);
103apply_mapping_and_ordering([{Old, false} | Rest], Doc, Result)
104  when is_atom(Old) ->
105    Doc1 = maps:remove(Old, Doc),
106    apply_mapping_and_ordering(Rest, Doc1, Result);
107apply_mapping_and_ordering([{Old, New} | Rest], Doc, Result)
108  when is_atom(Old) andalso is_atom(New) ->
109    case maps:is_key(Old, Doc) of
110        true ->
111            Value = maps:get(Old, Doc),
112            Doc1 = maps:remove(Old, Doc),
113            Result1 = [{New, Value} | Result],
114            apply_mapping_and_ordering(Rest, Doc1, Result1);
115        false ->
116            apply_mapping_and_ordering(Rest, Doc, Result)
117    end;
118apply_mapping_and_ordering([], Doc, Result) ->
119    lists:reverse(Result) ++ maps:to_list(Doc).
120