Current section

Files

Jump to
logger_formatter_json src logger_formatter_json.erl
Raw

src/logger_formatter_json.erl

%% @doc
%% Formatter for the Erlang logger library which outputs JSON.
%% https://www.erlang.org/doc/apps/kernel/logger_chapter.html#formatters
%%
%% @end
%%
-module(logger_formatter_json).
-export([format/2]).
-export([check_config/1]).
-ifdef(TEST).
-export([format_msg/3, to_string/2, to_thoas/1, is_printable/1, printable_list/1,
print_string/2]).
-endif.
%%%-----------------------------------------------------------------
%%% Types
-type config() ::
#{chars_limit => pos_integer() | unlimited,
depth => pos_integer() | unlimited,
map_msg => atom(),
max_size => pos_integer() | unlimited,
names => map() | atom() | [atom() | map()],
types => map() | [map()],
report_cb => logger:report_cb(),
single_line => boolean(),
template => template(),
time_designator => byte(),
time_offset => integer() | [byte()]}.
-type template() ::
[metakey() |
{metakey(), template(), template()} |
{group, metakey(), atom(), [atom()]} |
{group, metakey(), atom(), [atom()], map()}].
-type metakey() :: atom() | [atom()].
-define(IS_STRING(String), is_list(String) orelse is_binary(String)).
%%%-----------------------------------------------------------------
%%% API
-spec format(LogEvent, Config) -> unicode:chardata()
when LogEvent :: logger:log_event(),
Config :: config().
format(#{level := Level,
msg := {report, V} = _Msg,
meta := Meta},
#{map_msg := merge} = Config0)
when is_map(V) ->
Config = add_default_config(Config0),
Template0 = maps:get(template, Config),
Template =
lists:dropwhile(fun (msg) ->
true;
(_) ->
false
end,
Template0),
Result0 = [do_format(Level, maps:merge(Meta, V), Template, [], Config)],
Result = lists:flatten(Result0),
[thoas:encode_to_iodata(Result, #{escape => unicode}), "\n"];
format(#{level := Level,
msg := Msg,
meta := Meta},
Config0)
when is_map(Config0) ->
Config = add_default_config(Config0),
Template = maps:get(template, Config),
Result0 =
[do_format(Level,
maps:put(msg, format_msg(Msg, Meta, Config), Meta),
Template,
[],
Config)],
Result = lists:flatten(Result0),
[thoas:encode_to_iodata(Result, #{escape => unicode}), "\n"].
% @doc Map metadata key to JSON output name
-spec map_name(Key, Config) -> atom() | binary()
when Key :: atom(),
Config :: config().
map_name(Key, #{names := Names}) ->
maps:get(Key, Names, Key).
% @doc Map metadata value to a different type
-spec map_type(Key, Config) -> atom() | {atom(), atom()}
when Key :: atom(),
Config :: config().
map_type(Key, #{types := Types}) ->
maps:get(Key, Types, Key).
-spec do_format(Level, Data, Template, Seen, Config) -> [{atom() | binary(), binary()}]
when Level :: atom(),
Data :: map(),
Template :: template(),
Seen :: [atom() | [atom()]],
Config :: config().
do_format(Level, Data0, [all | _Format], _Seen, Config) ->
Data = maps:put(level, Level, Data0),
lists:map(fun({K, V}) -> {map_name(K, Config), to_output(K, V, Config)} end,
lists:keysort(1, maps:to_list(Data)));
do_format(Level, Data0, [rest | _Format], Seen, Config) ->
Data1 = maps:put(level, Level, Data0),
Data =
maps:without(
lists:flatten(Seen), Data1),
lists:map(fun({K, V}) -> {map_name(K, Config), to_output(K, V, Config)} end,
lists:keysort(1, maps:to_list(Data)));
do_format(Level, Data0, [{group, Key, Keys} | Format], Seen, Config) ->
do_format(Level, Data0, [{group, Key, Keys, #{}} | Format], Seen, Config);
do_format(Level, Data0, [{group, Key, Keys, GroupTypes} | Format], Seen, Config) ->
Types =
maps:merge(
maps:get(types, Config), GroupTypes),
Data = maps:with(Keys, Data0),
Data1 =
lists:map(fun({K, V}) -> {maps:get(K, Types, K), to_output(K, V, Config)} end,
lists:keysort(1, maps:to_list(Data))),
[{map_name(Key, Config), Data1} | do_format(Level, Data0, Format, [Keys | Seen], Config)];
do_format(Level, Data, [level | Format], Seen, Config) ->
[{map_name(level, Config), to_output(map_type(level, Config), Level, Config)}
| do_format(Level, Data, Format, [level | Seen], Config)];
do_format(Level, Data, [{Key, IfExist, Else} | Format], Seen, Config) ->
String0 =
case value(Key, Data) of
{ok, Value} ->
do_format(Level, Data#{Key => Value}, IfExist, Seen, Config);
error ->
do_format(Level, Data, Else, [Key | Seen], Config)
end,
case String0 of
[] ->
do_format(Level, Data, Format, Seen, Config);
String ->
[{map_name(Key, Config), String} | do_format(Level, Data, Format, Seen, Config)]
end;
do_format(Level, Data, [Key | Format], Seen, Config)
when is_atom(Key) orelse is_list(Key) andalso is_atom(hd(Key)) ->
String0 =
case value(Key, Data) of
{ok, Value} ->
to_output(map_type(Key, Config), Value, Config);
error ->
[]
end,
case String0 of
[] ->
do_format(Level, Data, Format, [Key | Seen], Config);
String ->
[{map_name(Key, Config), String} | do_format(Level, Data, Format, [Key | Seen], Config)]
end;
% do_format(Level,Data,[Str|Format],Config) ->
% [Str|do_format(Level,Data,Format,Config)];
do_format(_Level, _Data, [], _Seen, _Config) ->
[].
value(Key, Meta) when is_map_key(Key, Meta) ->
{ok, maps:get(Key, Meta)};
value([Key | Keys], Meta) when is_map_key(Key, Meta) ->
value(Keys, maps:get(Key, Meta));
value([], Value) ->
{ok, Value};
value(_, _) ->
error.
to_output({level, OutputFormat}, Value, Config) ->
format_level(OutputFormat, Value, Config);
to_output(system_time, Value, Config) ->
list_to_binary(format_time(Value, Config));
% to_string({system_time, OutputFormat},Value,Config) ->
% format_time(OutputFormat, Value,Config);
to_output(mfa, Value, Config) ->
list_to_binary(format_mfa(Value, Config));
to_output(_mfa, Value, _Config) ->
to_thoas(Value).
% to_string({level, OutputFormat}, Value, Config) ->
% format_level(OutputFormat, Value, Config);
% to_string(system_time, Value, Config) ->
% format_time(Value, Config);
% % to_string({system_time, OutputFormat},Value,Config) ->
% % format_time(OutputFormat, Value,Config);
% to_string(mfa, Value, Config) ->
% format_mfa(Value, Config);
% % to_string(crash_reason,Value,Config) ->
% % format_crash_reason(Value,Config);
% to_string(_, Value, Config) ->
% to_string(Value, Config).
to_string(X, _) when is_atom(X) ->
atom_to_list(X);
to_string(X, _) when is_integer(X) ->
integer_to_list(X);
to_string(X, _) when is_pid(X) ->
pid_to_list(X);
to_string(X, _) when is_reference(X) ->
ref_to_list(X);
% to_string(X, Config) when is_map(X) ->
% lists:map(fun({K, V}) -> {to_string(K, Config), to_string(V, Config)} end,
% lists:keysort(1, maps:to_list(X)));
% to_string({Key, Value}, Config) when is_map(Value) ->
% V2 = lists:map(fun({K, V}) -> {to_string(K, Config), to_string(V, Config)} end,
% lists:keysort(1, maps:to_list(Value))),
% to_string({Key, V2}, Config);
to_string(X, Config) when is_list(X) ->
case printable_list(lists:flatten(X)) of
true ->
X;
_ ->
io_lib:format(p(Config), [X])
end;
to_string(<<>>, _Config) ->
<<>>;
to_string(X, Config) when is_binary(X) ->
case is_printable(X) of
true ->
X;
_ ->
io_lib:format(p(Config), [X])
end;
to_string(X, Config) ->
io_lib:format(p(Config), [X]).
% Convert data to input that Thoas can handle
% is_atom/1
to_thoas(Value) when is_atom(Value) ->
% atom_to_list(Value);
Value;
% is_binary/1
to_thoas(<<>>) ->
<<>>;
to_thoas(Value) when is_binary(Value) ->
case is_printable(Value) of
true ->
Value;
_ ->
iolist_to_binary(io_lib:format("~w", [Value]))
end;
% is_bitstring/1
to_thoas(Value) when is_bitstring(Value) ->
% io_lib:format("~0tp", [Value]);
Value;
% is_boolean/1
to_thoas(Value) when is_boolean(Value) ->
Value;
% is_float/1
to_thoas(Value) when is_float(Value) ->
Value;
to_thoas(Value) when is_integer(Value) ->
Value;
% is_list/1
to_thoas([]) ->
[];
to_thoas(Value) when is_list(Value) ->
case printable_list(lists:flatten(Value)) of
true ->
% list_to_binary(Value);
iolist_to_binary(io_lib:format("~ts", [Value]));
_ ->
case Value of
[{_K, _V} | _Rest] ->
try
lists:map(fun({K, V}) -> {to_thoas(K), to_thoas(V)} end,
lists:keysort(1, Value))
catch
_:_ ->
iolist_to_binary(io_lib:format("~0tp", [Value]))
end;
_ ->
iolist_to_binary(io_lib:format("~0tp", [Value]))
end
end;
% is_pid/1
to_thoas(Value) when is_pid(Value) ->
list_to_binary(pid_to_list(Value));
% is_reference/1
to_thoas(Value) when is_reference(Value) ->
list_to_binary(ref_to_list(Value));
% is_map/1
to_thoas(Value) when is_map(Value) ->
lists:map(fun({K, V}) -> {to_thoas(K), to_thoas(V)} end,
lists:keysort(1, maps:to_list(Value)));
% thoas:encode(Value1, #{escape => unicode});
% is_number/1
to_thoas(Value) when is_number(Value) ->
Value;
% % is_tuple/1
to_thoas(Value) when is_tuple(Value) ->
lists:map(fun to_thoas/1, tuple_to_list(Value));
% iolist_to_binary(io_lib:format("{~0tp}", [Value1]);
% iolist_to_binary(["{", Value1, "}"]);
% iolist_to_binary(["{", thoas:encode_to_iodata(Value1, #{escape => unicode}), "}"]);
% is_tuple/1
% is_function/1
% is_function/2
% is_record/2
% is_record/3
% is_port/1
to_thoas(Value) ->
iolist_to_binary(io_lib:format("~0tp", [Value])).
-spec process_io_list(List, Config) -> binary()
when List :: list(),
Config :: config().
% process_io_list(List, Config) when is_list(List) ->
% Strings = lists:map(fun (X) -> iolist_to_string(X, Config) end, List),
% iolist_to_binary(Strings).
process_io_list([], _Config) ->
[];
process_io_list(List, Config) ->
process_io_list(List, [], Config).
process_io_list([], Acc, _Config) ->
iolist_to_binary(lists:reverse(Acc));
process_io_list([H | T], Acc, Config) when is_list(H) ->
process_io_list(T, [process_io_list(H, [], Config) | Acc], Config);
process_io_list([H | T], Acc, Config) ->
process_io_list(T, [iolist_to_string(H, Config) | Acc], Config);
% Handle improper list, e.g., [<<"foo">> | <<"bar">>
% Crash reports can contain improper lists
process_io_list(T, Acc, Config) ->
process_io_list([], [iolist_to_string(T, Config) | Acc], Config).
% @doc Format embedded things in iolist that iolist_to_binary chokes on
% https://www.erlang.org/doc/system/expressions.html
% is_atom/1
% is_binary/1
% is_bitstring/1
% is_boolean/1
% is_float/1
% is_function/1
% is_function/2
% is_integer/1
% is_list/1
% is_map/1
% is_number/1
% is_pid/1
% is_port/1
% is_record/2
% is_record/3
% is_reference/1
% is_tuple/1
iolist_to_string(X, _) when is_atom(X) ->
atom_to_list(X);
iolist_to_string(X, _) when is_integer(X) ->
X;
iolist_to_string(X, _) when is_pid(X) ->
pid_to_list(X);
iolist_to_string(X, _) when is_reference(X) ->
ref_to_list(X);
iolist_to_string(<<>>, _) ->
<<>>;
iolist_to_string(X, Config) when is_binary(X) ->
case is_printable(X) of
true ->
X;
_ ->
io_lib:format(p(Config), [X])
end;
iolist_to_string(X, Config) when is_list(X) ->
% case printable_list(lists:flatten(X)) of
case printable_list(X) of
true ->
X;
_ ->
io_lib:format(p(Config), [X])
end;
iolist_to_string(X, Config) ->
io_lib:format(p(Config), [X]).
-spec printable_list(list()) -> boolean().
% Print empty string as empty list
printable_list([]) ->
false;
printable_list(X) ->
io_lib:printable_unicode_list(X).
-spec is_printable(term()) -> boolean().
is_printable(X) when is_binary(X) ->
% case re:run(X, <<"[[:^print:]]">>, [{capture, none}, unicode]) of
% match -> false;
% _ -> true
% end;
io_lib:printable_unicode_list(
unicode:characters_to_list(X, unicode)).
% Format strings for print
print_string(X, Config) when is_binary(X) ->
case is_printable(X) of
true ->
X;
_ ->
io_lib:format(p(Config), [X])
end;
print_string(X, Config) when is_list(X) ->
case printable_list(lists:flatten(X)) of
true ->
list_to_binary(X);
_ ->
io_lib:format(p(Config), [X])
end.
-spec format_msg(Msg, Meta, Config) -> binary() | map()
when Msg ::
{io:format(), [term()]} | {report, logger:report()} | {string, unicode:chardata()},
Meta :: logger:metadata(),
Config :: config().
% format_msg({string, Chardata}, Meta, Config) -> format_msg({"~ts", [Chardata]}, Meta, Config);
format_msg({string, Chardata}, Meta, Config) when is_binary(Chardata) ->
case is_printable(Chardata) of
true ->
format_msg({"~ts", [Chardata]}, Meta, Config);
false ->
format_msg({"~tp", [Chardata]}, Meta, Config)
end;
format_msg({string, Chardata}, Meta, Config) ->
case io_lib:printable_unicode_list(Chardata) of
true ->
format_msg({"~ts", [Chardata]}, Meta, Config);
false ->
try
format_msg({"~ts", [process_io_list(Chardata, Config)]}, Meta, Config)
catch
% _:_ ->
% format_msg({"~tp", [Chardata]}, Meta, Config)
C:R:S ->
P = p(Config),
format_msg({"FORMAT CRASH: " ++ P ++ "; Reason: " ++ P,
% [Chardata, {C, R, logger:filter_stacktrace(?MODULE, S)}]
[Chardata, {C, R, S}]},
Meta,
Config)
end
end;
format_msg({report, Report}, _Meta, Config) when is_map(Report) ->
maps:map(fun(_K, V) -> print_string(to_string(V, Config), Config) end, Report);
format_msg({report, _} = Msg, Meta, #{report_cb := Fun} = Config)
when is_function(Fun, 1); is_function(Fun, 2) ->
format_msg(Msg, Meta#{report_cb => Fun}, maps:remove(report_cb, Config));
format_msg({report, Report}, #{report_cb := Fun} = Meta, Config)
when is_function(Fun, 1) ->
try Fun(Report) of
{Format, Args} when is_list(Format), is_list(Args) ->
format_msg({Format, Args}, maps:remove(report_cb, Meta), Config);
Other ->
P = p(Config),
format_msg({"REPORT_CB/1 ERROR: " ++ P ++ "; Returned: " ++ P, [Report, Other]},
Meta,
Config)
catch
C:R:S ->
P = p(Config),
format_msg({"REPORT_CB/1 CRASH: " ++ P ++ "; Reason: " ++ P,
[Report, {C, R, logger:filter_stacktrace(?MODULE, S)}]},
Meta,
Config)
end;
format_msg({report, Report}, #{report_cb := Fun} = Meta, Config)
when is_function(Fun, 2) ->
try Fun(Report, maps:with([depth, chars_limit, single_line], Config)) of
Chardata when ?IS_STRING(Chardata) ->
try
% already size limited by report_cb
chardata_to_list(Chardata)
catch
_:_ ->
P = p(Config),
format_msg({"REPORT_CB/2 ERROR: " ++ P ++ "; Returned: " ++ P,
[Report, Chardata]},
Meta,
Config)
end;
Other ->
P = p(Config),
format_msg({"REPORT_CB/2 ERROR: " ++ P ++ "; Returned: " ++ P, [Report, Other]},
Meta,
Config)
catch
C:R:S ->
P = p(Config),
format_msg({"REPORT_CB/2 CRASH: " ++ P ++ "; Reason: " ++ P,
[Report, {C, R, logger:filter_stacktrace(?MODULE, S)}]},
Meta,
Config)
end;
% format_msg({report,#{label:={error_logger,_}, format:=Format, args:=Args},Meta,Config) ->
% format_msg({Format, Args}, Meta, Config);
format_msg({report, Report}, Meta, Config) ->
format_msg({report, Report}, Meta#{report_cb => fun logger:format_report/1}, Config);
format_msg(Msg,
_Meta,
#{depth := Depth,
chars_limit := CharsLimit,
single_line := Single}) ->
Opts = chars_limit_to_opts(CharsLimit),
format_msg(Msg, Depth, Opts, Single).
-spec chars_limit_to_opts(CharsLimit :: unlimited | non_neg_integer()) ->
proplists:proplist().
chars_limit_to_opts(unlimited) ->
[];
chars_limit_to_opts(CharsLimit) ->
[{chars_limit, CharsLimit}].
format_msg({Format0, Args}, Depth, Opts, Single) ->
try
Format1 = io_lib:scan_format(Format0, Args),
Format = reformat(Format1, Depth, Single),
BuildText = io_lib:build_text(Format, Opts),
unicode:characters_to_binary(BuildText)
catch
C:R:S ->
P = p(Single),
FormatError = "FORMAT ERROR: " ++ P ++ " - " ++ P,
case Format0 of
FormatError ->
%% already been here - avoid failing cyclically
erlang:raise(C, R, S);
_ ->
format_msg({FormatError, [Format0, Args]}, Depth, Opts, Single)
end
end.
reformat(Format, unlimited, false) ->
Format;
reformat([#{control_char := C} = M | T], Depth, true) when C =:= $p ->
[limit_depth(M#{width => 0}, Depth) | reformat(T, Depth, true)];
reformat([#{control_char := C} = M | T], Depth, true) when C =:= $P ->
[M#{width => 0} | reformat(T, Depth, true)];
reformat([#{control_char := C} = M | T], Depth, Single) when C =:= $p; C =:= $w ->
[limit_depth(M, Depth) | reformat(T, Depth, Single)];
reformat([H | T], Depth, Single) ->
[H | reformat(T, Depth, Single)];
reformat([], _, _) ->
[].
limit_depth(M0, unlimited) ->
M0;
limit_depth(#{control_char := C0, args := Args} = M0, Depth) ->
%To uppercase.
C = C0 - ($a - $A),
M0#{control_char := C, args := Args ++ [Depth]}.
chardata_to_list(Chardata) ->
case unicode:characters_to_list(Chardata, unicode) of
List when is_list(List) ->
List;
Error ->
throw(Error)
end.
% https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
format_level(gcp, emergency, _Config) ->
<<"EMERGENCY">>;
format_level(gcp, alert, _Config) ->
<<"ALERT">>;
format_level(gcp, critical, _Config) ->
<<"CRITICAL">>;
format_level(gcp, error, _Config) ->
<<"ERROR">>;
format_level(gcp, warning, _Config) ->
<<"WARNING">>;
format_level(gcp, notice, _Config) ->
<<"INFO">>;
format_level(gcp, info, _Config) ->
<<"INFO">>;
format_level(gcp, debug, _Config) ->
<<"DEBUG">>;
format_level(gcp, _, _Config) ->
<<"DEFAULT">>.
%% SysTime is the system time in microseconds
format_time(SysTime, Config) when is_integer(SysTime) ->
#{time_offset := Offset, time_designator := Des} = Config,
calendar:system_time_to_rfc3339(SysTime,
[{unit, microsecond},
{offset, Offset},
{time_designator, Des}]).
format_mfa({M, F, A}, _) when is_atom(M), is_atom(F), is_integer(A) ->
io_lib:fwrite("~tw:~tw/~w", [M, F, A]);
format_mfa({M, F, A}, Config) when is_atom(M), is_atom(F), is_list(A) ->
format_mfa({M, F, length(A)}, Config);
format_mfa(MFA, Config) ->
to_string(MFA, Config).
% format_crash_reason({throw, Reason} ->
% format_crash_reason({exit, Reason} ->
% format_crash_reason({error, Exception, Stacktrace} ->
%% Ensure that all valid configuration parameters exist in the final
%% configuration map
-spec add_default_config(Config) -> config() when Config :: logger:formatter_config().
add_default_config(Config0) ->
Default =
#{chars_limit => unlimited,
% error_logger_notice_header => info,
% legacy_header => false,
single_line => true,
time_designator => $T},
MaxSize = get_max_size(maps:get(max_size, Config0, undefined)),
Depth = get_depth(maps:get(depth, Config0, undefined)),
Offset = get_offset(maps:get(time_offset, Config0, undefined)),
Names = get_names(maps:get(names, Config0, undefined)),
Types = get_types(maps:get(types, Config0, undefined)),
Template = expand_templates(maps:get(template, Config0, [{keys, all}])),
MapMsg = maps:get(map_msg, Config0, merge),
maps:merge(Default,
Config0#{max_size => MaxSize,
depth => Depth,
names => Names,
types => Types,
template => Template,
time_offset => Offset,
map_msg => MapMsg}).
expand_templates(Templates0) ->
Templates1 = lists:map(fun default_template/1, Templates0),
Templates2 = remove_dups(Templates1),
lists:flatten(Templates2).
default_template({keys, all}) ->
[msg, rest];
default_template({keys, basic}) ->
[time, level, msg];
default_template({keys, trace}) ->
[trace_id, span_id];
default_template({keys, otel}) ->
[otel_trace_id, otel_span_id, otel_trace_flags];
default_template({keys, gcp}) ->
[msg,
time,
level,
trace_id,
span_id,
{group, source_location, [file, line, mfa]},
{group, tags, [rest]}];
default_template(Value) ->
Value.
get_max_size(undefined) ->
unlimited;
get_max_size(S) ->
max(10, S).
get_depth(undefined) ->
error_logger:get_format_depth();
get_depth(S) ->
max(5, S).
-spec get_names(Names) -> map() when Names :: atom() | map() | [atom() | map()].
get_names(Names) when is_list(Names) ->
lists:foldl(fun(M, Acc) -> maps:merge(Acc, default_names(M)) end, #{}, Names);
get_names(Names) ->
default_names(Names).
-spec default_names(Names) -> map() when Names :: atom() | map().
default_names(Names) when is_map(Names) ->
Names;
default_names(datadog) ->
% https://docs.datadoghq.com/logs/log_configuration/processors/
% https://docs.datadoghq.com/logs/log_configuration/attributes_naming_convention/#source-code
% https://docs.datadoghq.com/tracing/faq/why-cant-i-see-my-correlated-logs-in-the-trace-id-panel/?tab=jsonlogs
#{time => <<"date">>,
level => <<"status">>,
msg => <<"message">>,
trace_id => <<"dd.trace_id">>,
span_id => <<"dd.span_id">>,
% level => <<"syslog.severity">>,
% time => <<"syslog.timestamp">>,
file => <<"logger.file_name">>,
mfa => <<"logger.method_name">>,
pid => <<"logger.thread_name">>};
% error.kind string The error type or kind (or code in some cases).
% error.message string A concise, human-readable, one-line message explaining the event.
% error.stack string The stack trace or the complementary information about the error.
default_names(gcp) ->
% https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry
% TODO: this is incomplete
#{time => <<"timestamp">>,
level => <<"severity">>,
msg => <<"textPayload">>,
trace_id => <<"trace">>,
span_id => <<"spanId">>,
source_location => <<"sourceLocation">>,
[source_location, file] => <<"file">>,
[source_location, line] => <<"line">>,
[source_location, mfa] => <<"function">>};
default_names(_) ->
#{}.
-spec get_types(Names) -> map() when Names :: atom() | map() | [atom() | map()].
get_types(Types) when is_list(Types) ->
Defaults =
#{time => system_time,
level => level,
mfa => mfa,
initial_call => mfa},
lists:foldl(fun(M, Acc) -> maps:merge(Acc, default_types(M)) end, Defaults, Types);
get_types(Types) ->
default_types(Types).
-spec default_types(Types) -> map() when Types :: atom() | map().
default_types(Types) when is_map(Types) ->
Types;
default_types(undefined) ->
#{time => system_time,
level => level,
mfa => mfa,
initial_call => mfa};
default_types(gcp) ->
% https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
#{level => {level, gcp}};
default_types(_) ->
#{}.
get_offset(undefined) ->
utc_to_offset(get_utc_config());
get_offset(Offset) ->
Offset.
utc_to_offset(true) ->
"Z";
utc_to_offset(false) ->
"".
get_utc_config() ->
%% SASL utc_log overrides stdlib config - in order to have uniform
%% timestamps in log messages
case application:get_env(sasl, utc_log) of
{ok, Val} when is_boolean(Val) ->
Val;
_ ->
case application:get_env(stdlib, utc_log) of
{ok, Val} when is_boolean(Val) ->
Val;
_ ->
false
end
end.
-spec check_config(Config) -> ok | {error, term()} when Config :: config().
check_config(Config) when is_map(Config) ->
do_check_config(maps:to_list(Config));
check_config(Config) ->
{error, {invalid_formatter_config, ?MODULE, Config}}.
do_check_config([{Type, L} | Config])
when Type == chars_limit; Type == depth; Type == max_size ->
case check_limit(L) of
ok ->
do_check_config(Config);
error ->
{error, {invalid_formatter_config, ?MODULE, {Type, L}}}
end;
do_check_config([{map_msg, embed} | Config]) ->
do_check_config(Config);
do_check_config([{map_msg, merge} | Config]) ->
do_check_config(Config);
do_check_config([{map_msg, Format} | _Config]) ->
{error, {invalid_formatter_config, ?MODULE, {map_msg, Format}}};
do_check_config([{single_line, SL} | Config]) when is_boolean(SL) ->
do_check_config(Config);
do_check_config([{legacy_header, LH} | Config]) when is_boolean(LH) ->
do_check_config(Config);
% do_check_config([{error_logger_notice_header, ELNH} | Config])
% when ELNH == info; ELNH == notice ->
% do_check_config(Config);
do_check_config([{report_cb, RCB} | Config])
when is_function(RCB, 1); is_function(RCB, 2) ->
do_check_config(Config);
do_check_config([{template, T} | Config]) ->
case check_template(T) of
ok ->
do_check_config(Config);
error ->
{error, {invalid_formatter_template, ?MODULE, T}}
end;
do_check_config([{time_offset, Offset} | Config]) ->
case check_offset(Offset) of
ok ->
do_check_config(Config);
error ->
{error, {invalid_formatter_config, ?MODULE, {time_offset, Offset}}}
end;
do_check_config([{names, Names} | Config]) ->
case check_names(Names) of
ok ->
do_check_config(Config);
error ->
{error, {invalid_formatter_config, ?MODULE, {names, Names}}}
end;
do_check_config([{types, Names} | Config]) ->
case check_types(Names) of
ok ->
do_check_config(Config);
error ->
{error, {invalid_formatter_config, ?MODULE, {types, Names}}}
end;
do_check_config([{time_designator, Char} | Config]) when Char >= 0, Char =< 255 ->
case io_lib:printable_latin1_list([Char]) of
true ->
do_check_config(Config);
false ->
{error, {invalid_formatter_config, ?MODULE, {time_designator, Char}}}
end;
do_check_config([C | _]) ->
{error, {invalid_formatter_config, ?MODULE, C}};
do_check_config([]) ->
ok.
check_limit(L) when is_integer(L), L > 0 ->
ok;
check_limit(unlimited) ->
ok;
check_limit(_) ->
error.
check_template([Key | T]) when is_atom(Key) ->
check_template(T);
check_template([Key | T]) when is_list(Key), is_atom(hd(Key)) ->
case lists:all(fun (X) when is_atom(X) ->
true;
(_) ->
false
end,
Key)
of
true ->
check_template(T);
false ->
error
end;
check_template([{Key, IfExist, Else} | T])
when is_atom(Key) orelse is_list(Key) andalso is_atom(hd(Key)) ->
case check_template(IfExist) of
ok ->
case check_template(Else) of
ok ->
check_template(T);
error ->
error
end;
error ->
error
end;
check_template([Str | T]) when is_list(Str) ->
case io_lib:printable_unicode_list(Str) of
true ->
check_template(T);
false ->
error
end;
check_template([Bin | T]) when is_binary(Bin) ->
case unicode:characters_to_list(Bin) of
Str when is_list(Str) ->
check_template([Str | T]);
_Error ->
error
end;
check_template([]) ->
ok;
check_template(_) ->
error.
check_names(datadog) ->
ok;
check_names(gcp) ->
ok;
check_names(Names) when is_atom(Names) ->
error;
check_names(Names) when is_map(Names) ->
ok;
check_names(Names) when is_list(Names) ->
case lists:all(fun(N) -> check_names(N) == ok end, Names) of
true ->
ok;
false ->
error
end;
check_names(_) ->
error.
check_types(gcp) ->
ok;
check_types(Types) when is_atom(Types) ->
error;
check_types(Types) when is_map(Types) ->
ok;
check_types(Types) when is_list(Types) ->
case lists:all(fun(N) -> check_types(N) == ok end, Types) of
true ->
ok;
false ->
error
end;
check_types(_) ->
error.
check_offset(I) when is_integer(I) ->
ok;
check_offset(Tz) when Tz =:= ""; Tz =:= "Z"; Tz =:= "z" ->
ok;
check_offset([Sign | Tz]) when Sign =:= $+; Sign =:= $- ->
check_timezone(Tz);
check_offset(_) ->
error.
check_timezone(Tz) ->
try io_lib:fread("~d:~d", Tz) of
{ok, [_, _], []} ->
ok;
_ ->
error
catch
_:_ ->
error
end.
p(#{single_line := Single}) ->
p(Single);
p(true) ->
"~0tp";
p(false) ->
"~tp".
remove_dups([]) ->
[];
remove_dups([H | T]) ->
[H | [X || X <- remove_dups(T), X /= H]].