%%% @doc %%% This is the main module that exposes custom formatting to the OTP %%% logger library (part of the `kernel' application since OTP-21). %%% %%% The module honors the standard configuration of the kernel's default %%% logger formatter regarding: max depth, templates. %%% @end -module(flatlog). %% API exports -export([format/2]). -ifdef(TEST). -export([format_msg/2, to_string/2]). -endif. -type template() :: [metakey() | {metakey(), template(), template()} | string()]. -type metakey() :: atom() | [atom()]. %%==================================================================== %% API functions %%==================================================================== -spec format(LogEvent, Config) -> unicode:chardata() when LogEvent :: logger:log_event(), Config :: logger:formatter_config(). format(Map = #{msg := {report, #{label := {error_logger, _}, format := Format, args := Terms}}}, UsrConfig) -> format(Map#{msg := {report, #{unstructured_log => unicode:characters_to_binary(io_lib:format(Format, Terms))}}}, UsrConfig); format(#{level:=Level, msg:={report, Msg}, meta:=Meta}, UsrConfig) when is_map(Msg) -> Config = apply_defaults(UsrConfig), NewMeta = maps:merge(Meta, #{level => Level ,colored_start => Level ,colored_end => "\e[0m" }), format_log(maps:get(template, Config), Config, Msg, NewMeta); format(Map = #{msg := {report, KeyVal}}, UsrConfig) when is_list(KeyVal) -> format(Map#{msg := {report, maps:from_list(KeyVal)}}, UsrConfig); format(Map = #{msg := {string, String}}, UsrConfig) -> format(Map#{msg := {report, #{unstructured_log => unicode:characters_to_binary(String)}}}, UsrConfig); format(Map = #{msg := {Format, Terms}}, UsrConfig) -> format(Map#{msg := {report, #{unstructured_log => unicode:characters_to_binary(io_lib:format(Format, Terms))}}}, UsrConfig). %%==================================================================== %% Internal functions %%==================================================================== apply_defaults(Map) -> maps:merge( #{term_depth => undefined, map_depth => -1, time_offset => 0, time_designator => $T, colored => false, colored_debug => "\e[0;38m", colored_info => "\e[1;37m", colored_notice => "\e[1;36m", colored_warning => "\e[1;33m", colored_error => "\e[1;31m", colored_critical => "\e[1;35m", colored_alert => "\e[1;44m", colored_emergency => "\e[1;41m", template => [colored_start, "when=", time, " level=", level, {id, [" id=", id], ""}, {parent_id, [" parent_id=", parent_id], ""}, {correlation_id, [" correlation_id=", correlation_id], ""}, {pid, [" pid=", pid], ""}, " at=", mfa, ":", line, colored_end, " ", msg, "\n"] }, Map ). -spec format_log(template(), Config, Msg, Meta) -> unicode:chardata() when Config :: logger:formatter_config(), Msg :: Data, Meta :: Data, Data :: #{string() | binary() | atom() => term()}. format_log(Tpl, Config, Msg, Meta) -> format_log(Tpl, Config, Msg, Meta, []). format_log([], _Config, _Msg, _Meta, Acc) -> lists:reverse(Acc); format_log([msg | Rest], Config, Msg, Meta, Acc) -> format_log(Rest, Config, Msg, Meta, [format_msg(Msg, Config) | Acc]); format_log([Key | Rest], Config, Msg, Meta, Acc) when is_atom(Key) orelse is_atom(hd(Key)) -> % from OTP case maps:find(Key, Meta) of error -> format_log(Rest, Config, Msg, Meta, Acc); {ok, Val} -> format_log(Rest, Config, Msg, Meta, [format_val(Key, Val, Config) | Acc]) end; format_log([{Key, IfExists, Else} | Rest], Config, Msg, Meta, Acc) -> case maps:find(Key, Meta) of error -> format_log(Rest, Config, Msg, Meta, [Else | Acc]); {ok, Val} -> format_log(Rest, Config, Msg, Meta, [format_log(IfExists, Config, Msg, #{Key => Val}, []) | Acc]) end; format_log([Term | Rest], Config, Msg, Meta, Acc) when is_list(Term) -> format_log(Rest, Config, Msg, Meta, [Term | Acc]). format_msg(Data, Config) -> format_msg("", Data, Config). format_msg(Parents, Data, Config=#{map_depth := 0}) when is_map(Data) -> to_string(truncate_key(Parents), Config)++"=... "; format_msg(Parents, Data, Config = #{map_depth := Depth}) when is_map(Data) -> maps:fold( fun(K, V, Acc) when is_map(V) -> [format_msg(Parents ++ to_string(K, Config) ++ "_", V, Config#{map_depth := Depth-1}) | Acc] ; (K, V, Acc) -> [Parents ++ to_string(K, Config), $=, to_string(V, Config), $\s | Acc] end, [], Data ). format_val(time, Time, Config) -> format_time(Time, Config); format_val(mfa, MFA, Config) -> escape(format_mfa(MFA, Config)); format_val(colored_end, _EOC, #{colored := false}) -> ""; format_val(colored_end, EOC, #{colored := true}) -> EOC; format_val(colored_start, _Level, #{colored := false}) -> ""; format_val(colored_start, debug, #{colored := true, colored_debug := BOC}) -> BOC; format_val(colored_start, info, #{colored := true, colored_info := BOC}) -> BOC; format_val(colored_start, notice, #{colored := true, colored_notice := BOC}) -> BOC; format_val(colored_start, warning, #{colored := true, colored_warning := BOC}) -> BOC; format_val(colored_start, error, #{colored := true, colored_error := BOC}) -> BOC; format_val(colored_start, critical, #{colored := true, colored_critical := BOC}) -> BOC; format_val(colored_start, alert, #{colored := true, colored_alert := BOC}) -> BOC; format_val(colored_start, emergency, #{colored := true, colored_emergency := BOC}) -> BOC; format_val(_Key, Val, Config) -> to_string(Val, Config). format_time(N, #{time_offset := O, time_designator := D}) when is_integer(N) -> calendar:system_time_to_rfc3339(N, [{unit, microsecond}, {offset, O}, {time_designator, D}]). format_mfa({M, F, A}, _) when is_atom(M), is_atom(F), is_integer(A) -> [atom_to_list(M), $:, atom_to_list(F), $/, integer_to_list(A)]; format_mfa({M, F, A}, Config) when is_atom(M), is_atom(F), is_list(A) -> %% arguments are passed as a literal list ({mod, fun, [a, b, c]}) format_mfa({M, F, length(A)}, Config); format_mfa(MFAStr, Config) -> % passing in a pre-formatted string value to_string(MFAStr,Config). to_string(X, _) when is_atom(X) -> escape(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, C) when is_binary(X) -> case unicode:characters_to_list(X) of {_, _, _} -> % error or incomplete escape(format_str(C, X)); List -> case io_lib:printable_list(List) of true -> escape(List); _ -> escape(format_str(C, X)) end end; to_string(X, C) when is_list(X) -> case io_lib:printable_list(X) of true -> escape(X); _ -> escape(format_str(C, X)) end; to_string(X, C) -> escape(format_str(C, X)). format_str(#{term_depth := undefined}, T) -> io_lib:format("~0tp", [T]); format_str(#{term_depth := D}, T) -> io_lib:format("~0tP", [T, D]). escape(Str) -> case needs_escape(Str) of false -> case needs_quoting(Str) of true -> [$", Str, $"]; false -> Str end; true -> [$", do_escape(Str), $"] end. needs_quoting(Str) -> string:find(Str, " ") =/= nomatch orelse string:find(Str, "=") =/= nomatch. needs_escape(Str) -> string:find(Str, "\"") =/= nomatch orelse string:find(Str, "\\") =/= nomatch orelse string:find(Str, "\n") =/= nomatch. do_escape([]) -> []; do_escape(Str) -> case string:next_grapheme(Str) of [$\n | Rest] -> [$\\, $\n | do_escape(Rest)]; ["\r\n" | Rest] -> [$\\, $\r, $\\, $\n | do_escape(Rest)]; [$" | Rest] -> [$\\, $" | do_escape(Rest)]; [$\\ | Rest] -> [$\\, $\\ | do_escape(Rest)]; [Grapheme | Rest] -> [Grapheme | do_escape(Rest)] end. truncate_key([]) -> []; truncate_key("_") -> ""; truncate_key([H|T]) -> [H | truncate_key(T)].