aboutsummaryrefslogblamecommitdiffstats
path: root/lib/kernel/src/logger_formatter.erl
blob: 91283ab2995c08620d846cb1fa6075f37d475f70 (plain) (tree)






























































































































































































































































                                                                                    
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2017. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
-module(logger_formatter).

-export([format/2]).

-include("logger_internal.hrl").

%%%-----------------------------------------------------------------
%%% Types
-type template() :: [atom()|tuple()|string()].

%%%-----------------------------------------------------------------
%%% API
-spec format(Log,Config) -> String when
      Log :: logger:log(),
      Config :: #{single_line=>boolean(),
                  legacy_header=>boolean(),
                  report_cb=>fun((logger:report()) -> {io:format(),[term()]}),
                  max_size=>pos_integer() | unlimited,
                  depth=>pos_integer() | unlimited,
                  template=>template(),
                  utc=>boolean()},
      String :: string().
format(#{level:=Level,msg:=Msg0,meta:=Meta},Config0)
  when is_map(Config0) ->
    Config = add_default_config(Config0),
    Meta1 = maybe_add_legacy_header(Level,Meta,Config),
    MsgStr0 = format_msg(Msg0,Meta1,Config),
    MsgStr =
        case maps:get(single_line,Config) of
            true ->
                %% Trim leading and trailing whitespaces, and replace
                %% newlines with ", "
                re:replace(string:trim(MsgStr0),",?\r?\n\s*",", ",
                           [{return,list},global]);
            _false2 ->
                MsgStr0
        end,
    String = do_format(Level,MsgStr,Meta1,maps:get(template,Config),Config),
    truncate(String,maps:get(max_size,Config)).

do_format(Level,Msg,Data,[level|Format],Config) ->
    [to_string(level,Level,Config)|do_format(Level,Msg,Data,Format,Config)];
do_format(Level,Msg,Data,[msg|Format],Config) ->
    [Msg|do_format(Level,Msg,Data,Format,Config)];
do_format(Level,Msg,Data,[Key|Format],Config) when is_atom(Key); is_tuple(Key) ->
    Value = value(Key,Data),
    [to_string(Key,Value,Config)|do_format(Level,Msg,Data,Format,Config)];
do_format(Level,Msg,Data,[Str|Format],Config) ->
    [Str|do_format(Level,Msg,Data,Format,Config)];
do_format(_Level,_Msg,_Data,[],_Config) ->
    [].

value(Key,Meta) when is_atom(Key), is_map(Meta) ->
    maps:get(Key,Meta,"");
value(Key,_) when is_atom(Key) ->
    "";
value(Keys,Meta) when is_tuple(Keys) ->
    value(tuple_to_list(Keys),Meta);
value([Key|Keys],Meta) ->
    value(Keys,value(Key,Meta));
value([],Value) ->
    Value.

to_string(time,Time,Config) ->
    format_time(Time,Config);
to_string(mfa,MFA,_Config) ->
    format_mfa(MFA);
to_string(_,Value,_Config) ->
    to_string(Value).

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) when is_list(X) ->
    case io_lib:printable_unicode_list(lists:flatten(X)) of
        true -> X;
        _ -> io_lib:format("~tp",[X])
    end;
to_string(X) ->
    io_lib:format("~tp",[X]).

format_msg({string,Chardata},Meta,Config) ->
    try unicode:characters_to_list(Chardata)
    catch _:_ -> format_msg({"INVALID STRING: ~tp",[Chardata]},Meta,Config)
    end;
format_msg({report,_}=Msg,Meta,#{report_cb:=Fun}=Config) when is_function(Fun,1) ->
    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 ->
            format_msg({"REPORT_CB ERROR: ~tp; Returned: ~tp",
                        [Report,Other]},Meta,Config)
    catch C:R ->
            format_msg({"REPORT_CB CRASH: ~tp; Reason: ~tp",
                        [Report,{C,R}]},Meta,Config)
    end;
format_msg({report,Report},Meta,Config) ->
    format_msg({report,Report},
               Meta#{report_cb=>fun logger:format_report/1},
               Config);
format_msg(Msg,_Meta,Config) ->
    limit_depth(Msg, maps:get(depth,Config)).

limit_depth(Msg,false) ->
    Depth = logger:get_format_depth(),
    limit_depth(Msg,Depth);
limit_depth({Format,Args},unlimited) ->
    try io_lib:format(Format,Args)
    catch _:_ ->
            io_lib:format("FORMAT ERROR: ~tp - ~tp",[Format,Args])
    end;
limit_depth({Format0,Args},Depth) ->
    try
        Format1 = io_lib:scan_format(Format0, Args),
        Format = limit_format(Format1, Depth),
        io_lib:build_text(Format)
    catch _:_ ->
            limit_depth({"FORMAT ERROR: ~tp - ~tp",[Format0,Args]},Depth)
    end.

limit_format([#{control_char:=C0}=M0|T], Depth) when C0 =:= $p;
						     C0 =:= $w ->
    C = C0 - ($a - $A),				%To uppercase.
    #{args:=Args} = M0,
    M = M0#{control_char:=C,args:=Args++[Depth]},
    [M|limit_format(T, Depth)];
limit_format([H|T], Depth) ->
    [H|limit_format(T, Depth)];
limit_format([], _) ->
    [].

truncate(String,unlimited) ->
    String;
truncate(String,false) ->
    Size = logger:get_max_size(),
    truncate(String,Size);
truncate(String,Size) ->
    Length = string:length(String),
    if Length>Size ->
            string:slice(String,0,Size-3)++"...";
       true ->
            String
    end.

format_time(Timestamp,Config) when is_integer(Timestamp) ->
    {Date,Time,Micro} = timestamp_to_datetimemicro(Timestamp,Config),
    format_time(Date,Time,Micro);
format_time(Other,_Config) ->
    %% E.g. a string
    to_string(Other).

format_time({Y,M,D},{H,Min,S},Micro) ->
    io_lib:format("~4w-~2..0w-~2..0w ~2w:~2..0w:~2..0w.~6..0w",
                  [Y,M,D,H,Min,S,Micro]).

%% Assuming this is monotonic time in microseconds
timestamp_to_datetimemicro(Timestamp,Config) when is_integer(Timestamp) ->
    SysTime = Timestamp + erlang:time_offset(microsecond),
    Micro = SysTime rem 1000000,
    Sec = SysTime div 1000000,
    UniversalTime =  erlang:posixtime_to_universaltime(Sec),
    {Date,Time} =
        case Config of
            #{utc:=true} -> UniversalTime;
            _ -> erlang:universaltime_to_localtime(UniversalTime)
        end,
    {Date,Time,Micro}.

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}) when is_atom(M), is_atom(F), is_list(A) ->
    format_mfa({M,F,length(A)});
format_mfa(MFA) ->
    to_string(MFA).

maybe_add_legacy_header(Level,
                        #{time:=Timestamp}=Meta,
                        #{legacy_header:=true}=Config) ->
    #{title:=Title}=MyMeta = add_legacy_title(Level,maps:get(?MODULE,Meta,#{})),
    {{Y,Mo,D},{H,Mi,S},Micro} = timestamp_to_datetimemicro(Timestamp,Config),
    Header = io_lib:format("=~ts==== ~w-~s-~4w::~2..0w:~2..0w:~2..0w.~6..0w ~s===",
                           [Title,D,month(Mo),Y,H,Mi,S,Micro,utcstr(Config)]),
    Meta#{?MODULE=>MyMeta#{header=>Header}};
maybe_add_legacy_header(_,Meta,_) ->
    Meta.

add_legacy_title(_Level,#{title:=_}=MyMeta) ->
    MyMeta;
add_legacy_title(Level,MyMeta) ->
    Title = string:uppercase(atom_to_list(Level)) ++ " REPORT",
    MyMeta#{title=>Title}.

month(1) -> "Jan";
month(2) -> "Feb";
month(3) -> "Mar";
month(4) -> "Apr";
month(5) -> "May";
month(6) -> "Jun";
month(7) -> "Jul";
month(8) -> "Aug";
month(9) -> "Sep";
month(10) -> "Oct";
month(11) -> "Nov";
month(12) -> "Dec".

utcstr(#{utc:=true}) -> "UTC ";
utcstr(_) -> "".

add_default_config(#{utc:=_}=Config) ->
    Default =
        #{legacy_header=>false,
          single_line=>false,
          max_size=>false,
          depth=>false},
    add_default_template(maps:merge(Default,Config));
add_default_config(Config) ->
    add_default_config(Config#{utc=>logger:get_utc_config()}).

add_default_template(#{template:=_}=Config) ->
    Config;
add_default_template(Config) ->
    Config#{template=>default_template(Config)}.

default_template(#{legacy_header:=true}) ->
    ?DEFAULT_FORMAT_TEMPLATE_HEADER;
default_template(#{single_line:=true}) ->
    ?DEFAULT_FORMAT_TEMPLATE_SINGLE;
default_template(_) ->
    ?DEFAULT_FORMAT_TEMPLATE.