1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @copyright 2020, Erlang Solutions Ltd. |
3 |
|
%%% @doc |
4 |
|
%%% A module formatting logger reports into JSON objects. |
5 |
|
%%% |
6 |
|
%%% @end |
7 |
|
%%%------------------------------------------------------------------- |
8 |
|
-module(mongoose_json_formatter). |
9 |
|
|
10 |
|
%% API |
11 |
|
-export([format/2]). |
12 |
|
|
13 |
|
-ignore_xref([format/2]). |
14 |
|
|
15 |
|
-spec format(logger:log_event(), logger:formatter_config()) -> unicode:chardata(). |
16 |
|
format(Event, FConfig) -> |
17 |
3568 |
try do_format(Event, FConfig) |
18 |
|
catch |
19 |
|
%% Errors during log formatting can lead to a death spiral of recursive error logging, so |
20 |
|
%% format the formatter error in a safe way and don't allow the exception to propagate. |
21 |
:-( |
error:Reason:Stacktrace -> format_log_formatter_error(error, Reason, Stacktrace, Event, FConfig) |
22 |
|
end. |
23 |
|
|
24 |
|
|
25 |
|
format_log_formatter_error(Class, Reason, Stacktrace, #{meta := #{time := Time}} = Event, FConfig) -> |
26 |
:-( |
IoData = jiffy:encode( |
27 |
|
#{ |
28 |
|
'when' => format_time(Time), |
29 |
|
level => error, |
30 |
|
what => log_format_failed, |
31 |
|
class => Class, |
32 |
|
reason => Reason, |
33 |
|
stacktrace => format_item(Stacktrace, FConfig), |
34 |
|
formatter_module => ?MODULE, |
35 |
|
original_event => unicode:characters_to_binary(io_lib:format("~0p", [Event])) |
36 |
|
} |
37 |
|
), |
38 |
:-( |
[IoData, <<"\n">>]. |
39 |
|
|
40 |
|
|
41 |
|
-spec do_format(logger:log_event(), logger:formatter_config()) -> unicode:chardata(). |
42 |
|
do_format(E = #{msg := {report, #{label := {error_logger, _}, format := Format, args := Terms}}}, FConfig) -> |
43 |
64 |
do_format(E#{msg := {report, |
44 |
|
#{unstructured_log => |
45 |
|
unicode:characters_to_binary(io_lib:format(Format, Terms))}}}, |
46 |
|
FConfig); |
47 |
|
do_format(E = #{msg := {string, String}}, FConfig) -> |
48 |
:-( |
do_format(E#{msg := {report, |
49 |
|
#{unstructured_log => |
50 |
|
unicode:characters_to_binary(io_lib:format(String, []))}}}, FConfig); |
51 |
|
do_format(E = #{msg := {report, Report}}, FConfig) when is_map(Report) -> |
52 |
3568 |
NewMap = process_metadata(E), |
53 |
3568 |
NewReport = maps:merge(Report, NewMap), |
54 |
3568 |
NewConfig = maps:merge(default_config(), config_correct_depth(FConfig)), |
55 |
3568 |
Formatted = format_item(NewReport, NewConfig), |
56 |
3568 |
IoData = jiffy:encode(Formatted), |
57 |
3568 |
[IoData, <<"\n">>]; |
58 |
|
do_format(Map = #{msg := {Format, Terms}}, FConfig) -> |
59 |
473 |
do_format(Map#{msg := {report, |
60 |
|
#{unstructured_log => |
61 |
|
unicode:characters_to_binary(io_lib:format(Format, Terms))}}}, |
62 |
|
FConfig). |
63 |
|
|
64 |
|
|
65 |
|
format_item(_Item, _FConfig = #{depth := 0}) -> |
66 |
:-( |
<<"...">>; |
67 |
|
format_item(Item, FConfig = #{depth := D}) when is_map(Item) -> |
68 |
9914 |
ML = [{all_to_binary(Key, FConfig), |
69 |
9914 |
format_item(Val, FConfig#{depth := D - 1})} || {Key, Val} <- maps:to_list(Item)], |
70 |
9914 |
maps:from_list(ML); |
71 |
|
format_item(Item, FConfig = #{depth := Depth}) when is_list(Item) -> |
72 |
8744 |
case io_lib:printable_unicode_list(Item) of |
73 |
|
true -> |
74 |
|
% We want to print strings as strings |
75 |
5321 |
format_str(Item, FConfig); |
76 |
|
false -> |
77 |
|
% And pass lists of objects as lists of objects |
78 |
3423 |
[format_item(I, FConfig#{depth := Depth - 1}) || I <- Item] |
79 |
|
end; |
80 |
|
format_item(Item, FConfig) -> |
81 |
42712 |
all_to_binary(Item, FConfig). |
82 |
|
|
83 |
|
format_time(T) -> |
84 |
3568 |
TimeString = calendar:system_time_to_rfc3339(T, [{unit, microsecond}]), |
85 |
3568 |
unicode:characters_to_binary(TimeString). |
86 |
|
|
87 |
|
all_to_binary(Full, FConfig) when is_binary(Full) -> |
88 |
6871 |
Short = shorten_binary(Full, FConfig), |
89 |
6871 |
ShortUnicode = unicode:characters_to_binary(Short, utf8, utf8), |
90 |
6871 |
FullUnicode = unicode:characters_to_binary(Short, utf8, utf8), |
91 |
6871 |
case {ShortUnicode, FullUnicode} of |
92 |
6850 |
{<<_/binary>>, <<_/binary>>} -> ShortUnicode; |
93 |
:-( |
{{incomplete,Incomplete,_}, <<_/binary>>} -> Incomplete; |
94 |
21 |
_ -> format_non_unicode(Full, FConfig) |
95 |
|
end; |
96 |
|
all_to_binary(Something, FConfig) -> |
97 |
81353 |
format_non_unicode(Something, FConfig). |
98 |
|
|
99 |
|
format_non_unicode(Something, FConfig) -> |
100 |
81374 |
Chars = format_str(Something, FConfig), |
101 |
81374 |
unicode:characters_to_binary(Chars, utf8). |
102 |
|
|
103 |
|
shorten_binary(S, #{format_chars_limit := unlimited}) -> |
104 |
:-( |
S; |
105 |
|
shorten_binary(S, #{format_chars_limit := L}) -> |
106 |
70573 |
binary_part(S, 0, min(size(S), L)). |
107 |
|
|
108 |
:-( |
format_chars_limit_to_opts(unlimited) -> []; |
109 |
22993 |
format_chars_limit_to_opts(CharsLimit) -> [{chars_limit, CharsLimit}]. |
110 |
|
|
111 |
|
format_str(S, FConfig) when is_list(S) -> |
112 |
63702 |
case io_lib:printable_unicode_list(S) of |
113 |
|
true -> |
114 |
63702 |
B = unicode:characters_to_binary(S, utf8), |
115 |
63702 |
shorten_binary(B, FConfig); |
116 |
|
false -> |
117 |
:-( |
do_format_str(S, FConfig) |
118 |
|
end; |
119 |
|
format_str(S, FConfig) when is_atom(S) -> |
120 |
58381 |
format_str(atom_to_list(S), FConfig); |
121 |
|
format_str(S, FConfig) -> |
122 |
22993 |
do_format_str(S, FConfig). |
123 |
|
|
124 |
|
do_format_str(S, #{format_depth := unlimited, format_chars_limit := L}) -> |
125 |
:-( |
io_lib:format("~0tp", [S], format_chars_limit_to_opts(L)); |
126 |
|
do_format_str(S, #{format_depth := D, format_chars_limit := L}) -> |
127 |
22993 |
io_lib:format("~0tP", [S, D], format_chars_limit_to_opts(L)). |
128 |
|
|
129 |
|
process_metadata(#{meta := #{time := T} = M, level := L}) -> |
130 |
3568 |
DiscardKeys = [time, gl, report_cb], |
131 |
3568 |
#{level => L, |
132 |
|
'when' => format_time(T), |
133 |
|
meta => maps:without(DiscardKeys, M)}. |
134 |
|
|
135 |
|
config_correct_depth(C = #{depth := unlimited}) -> |
136 |
:-( |
C#{depth := -1}; |
137 |
|
config_correct_depth(C) -> |
138 |
3568 |
C. |
139 |
|
|
140 |
|
default_config() -> |
141 |
3568 |
#{format_chars_limit => unlimited, |
142 |
|
format_depth => unlimited, |
143 |
|
depth => -1 |
144 |
|
}. |