Packages

A wrapper for tracing test runs using TTB

Current section

Files

Jump to
trace_runner src tr_ttb.erl
Raw

src/tr_ttb.erl

%% -*- mode: erlang; indent-tabs-mode: nil; -*-
%%---- BEGIN COPYRIGHT -------------------------------------------------------
%%
%% Copyright (C) 2016 Ulf Wiger. All rights reserved.
%%
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at http://mozilla.org/MPL/2.0/.
%%
%%---- END COPYRIGHT ---------------------------------------------------------
-module(tr_ttb).
-export([ event/1 ]).
-export([ dbg_tracer/0
, dbg_tracer/1
, dbg_stop/0 ]).
-export([
on_nodes/2,
on_nodes/3,
on_nodes/4,
default_patterns/0,
default_flags/0,
stop/0,
stop_nofetch/0,
format/1,
format/2,
format/3,
format_opts/0,
format_opts/1,
handler/4,
pp/3,
pp_term/2, %% (Term, Module | F((T) -> {yes, T1} | no)) -> Term1
pp_custom/3, %% (Term, Tag, Fmt((X) -> X1)) -> Term1
record_print_fun/1
]).
-export([ cfg/3 ]).
-export([ shell_records_tab/0 ]).
-export_type([ flag/0, flags/0,
pattern/0, patterns/0,
proc/0 ]).
-type trace_pat() :: any().
-type pattern() :: {module(), atom(), arity(), trace_pat()}.
-type patterns() :: [pattern()].
-type proc() :: pid() | port() | atom() | {global,any()}
| all | processes | ports | existing | existing_processes
| existing_ports | new | new_processes | new_ports.
-type procs() :: proc() | [proc()].
-type flag() :: clear | all
| m | s | r | c | call | p | sos | sol | sofs | sofl
| send | 'receive' | call | procs | ports | garbage_collection
| running | set_on_spawn | set_on_first_spawn | set_on_link
| set_on_first_link | timestamp | monotonic_timestamp
| strict_monotonic_timestamp | arity | return_to | silent
| running_procs | running_ports | exiting.
-type flags() :: flag() | [flag()].
-callback flags() -> {procs(), flags()}.
-callback patterns() -> [pattern()].
-define(EVENT(E, S), event(?LINE, E, S)).
%% This function is also traced. Can be used to insert markers in the trace
%% log.
event(E) ->
event(?LINE, E, none).
event(_, _, _) ->
ok.
dbg_tracer() ->
dbg_tracer(#{}).
dbg_tracer(Opts) when is_map(Opts) ->
St0 = init_state(maps:merge(#{ fd => group_leader()
, print_header => false }, Opts)),
dbg:tracer(process, {fun dhandler/2, St0}).
dbg_stop() ->
await_traces(),
dbg:stop().
shell_records_tab() ->
case find_shell_process() of
ShellPid when is_pid(ShellPid) ->
case [T || T <- ets:all(),
ets:info(T, owner) == ShellPid
andalso ets:info(T, name) == shell_records] of
[RecTab] ->
RecTab;
[] ->
undefined
end;
_ ->
undefined
end.
find_shell_process() ->
find_shell_process(group_leader()).
find_shell_process(GL) ->
case lists:keyfind(shell, 1, pi(GL, dictionary)) of
{_, Pid} ->
Pid;
false ->
case pi(GL, group_leader) of
GL -> undefined;
GL1 -> find_shell_process(GL1)
end
end.
pi(P, Key) when is_pid(P) ->
{_, I} = process_info(P, Key),
I.
await_traces() ->
case dbg:get_tracer() of
{ok, Tracer} ->
await_tracer(process_info(Tracer, message_queue_len), Tracer);
{error, _} ->
ok
end.
await_tracer({message_queue_len, L}, Tracer) when L > 3 ->
timer:sleep(10),
await_tracer(process_info(Tracer, message_queue_len), Tracer);
await_tracer(_, _) ->
ok.
on_nodes(Ns, File) ->
on_nodes(Ns, default_patterns(), default_flags(), [{file, File}]).
on_nodes(Ns, File, Spec) ->
on_nodes(Ns,
cfg(Spec, patterns, default_patterns()),
cfg(Spec, flags, default_flags()),
[ {file, File}
, {tr_ttb_info, cfg(Spec, info, undefined)}]).
-spec on_nodes([node()], [pattern()], {procs(), flags()}, list()) ->
{ok,list()} | {error, any()}.
on_nodes(Ns, Patterns0, Flags, Opts0) ->
Opts = lists:keydelete(tr_ttb_info, 1, Opts0),
Patterns = expand_patterns(Patterns0),
Res = ttb_start_trace(Ns, Patterns, Flags, Opts),
?EVENT(ttb_started, maybe_info(#{ nodes => Ns
, patterns => Patterns
, flags => Flags }, Opts0)),
Res.
maybe_info(Map, Opts) ->
case lists:keyfind(tr_ttb_info, 1, Opts) of
{_, Info} when Info =/= undefined ->
Map#{info => Info};
_ ->
Map
end.
ttb_start_trace(Ns, Patterns, Flags, Opts) ->
Start = maps:get(start, Patterns, []),
Res = ttb:start_trace(Ns, Start, Flags, Opts),
maps:fold(
fun(Op, Pats, _) when Op==tp; Op==tpl->
[ttb:Op(M,F,A,MS) || {M,F,A,MS} <- Pats];
(Op, Pats, _) when Op==ctp; Op==ctpl; Op==ctpg ->
[ttb:Op(M,F,A) || {M,F,A} <- Pats];
(tpe, Pats, _) ->
[dbg:tpe(E, MS) || {E, MS} <- Pats];
(ctpe, Pats, _) ->
[dbg:ctpe(E) || E <- Pats]
end, ok, maps:without([start], Patterns)),
Res.
expand_patterns(Patterns) when is_map(Patterns) ->
maps:merge(Patterns,
maps:map(fun(_K, V) when is_list(V) ->
[expand_pat(P) || P <- V];
(_, V) -> V
end,
maps:with([start,tp,tpl], Patterns)));
expand_patterns(Patterns) when is_list(Patterns) ->
#{start => [expand_pat(P) || P <- Patterns]}.
expand_pat(P) when is_tuple(P) ->
Sz = tuple_size(P),
case element(Sz, P) of
x ->
setelement(Sz, P, [{'_',[],[{exception_trace}]}]);
_ ->
P
end;
expand_pat(P) ->
P.
-spec default_patterns() -> [pattern()].
default_patterns() ->
[{?MODULE , event, 3, []}].
-spec default_flags() -> {procs(), flags()}.
default_flags() ->
{all, call}.
stop() ->
{stopped, Dir} = ttb:stop([return_fetch_dir]),
Dir.
stop_nofetch() ->
ttb:stop([nofetch]).
format(Dir) ->
format(Dir, standard_io).
format(Dir, OutFile) ->
format(Dir, OutFile, #{}).
format(Dir, OutFile, Opts) ->
try ttb:format(Dir, format_opts(OutFile, Opts))
catch
error:exceeded_limit = Reason ->
{error, Reason};
error:Other:ST ->
{error, {Other, ST}}
end.
format_opts() ->
format_opts(standard_io).
format_opts(Outfile) ->
format_opts(Outfile, #{}).
format_opts(OutFile, Opts0) ->
Opts = maps:merge(#{limit => 10000}, Opts0),
[{out, OutFile}, {handler, {fun handler/4, init_state(Opts)}}].
init_state(Opts0) ->
Opts = maps:merge(#{ ts => 0
, diff => 0
, limit => infinity
, sofar => 0
, opts => Opts0 }, Opts0),
case Opts of
#{shell_records := true, shell_records_tab := _} ->
Opts;
#{shell_records := true} ->
case shell_records_tab() of
undefined ->
maps:without([shell_records, shell_records_tab], Opts);
Tab ->
Opts#{shell_records_tab => Tab}
end;
_ ->
Opts
end.
%% Real-time handler (see dbg_tracer/1
dhandler(end_of_trace, St) ->
St;
dhandler(Trace, #{fd := Fd} = St) ->
handler(Fd, Trace, [], St).
handler(Fd, Trace, TI, #{delay := D} = Acc) ->
timer:sleep(D),
handler(Fd, Trace, TI, maps:remove(delay, Acc));
handler(_, _, _, #{limit_exceeded := true} = Acc) ->
Acc;
handler(Fd, Trace, TI, Acc) ->
try Res = handler_(Fd, Trace, TI, Acc),
Res
catch
error:limit_exceeded ->
Acc#{limit_exceeded => true};
Caught:E:ST ->
fwrite(user, "CAUGHT ~p:~p:~p~nTrace=~p", [Caught, E, ST, Trace]),
Acc
end.
handler_(Fd, Trace, _, #{ts := TSp, diff := Diff, sofar := Sofar} = Acc) ->
TS = ts(Trace, TSp),
L0 = case {TSp,Diff} of {0,0} ->
case maps:get(print_header, Acc, true) of
true ->
io:fwrite(Fd, "%% -*- erlang -*-~n", []),
io:put_chars(Fd, format_time(TS)),
2;
_ ->
0
end;
_ -> 0
end,
Tdiff = tdiff(TS, TSp, time_resolution(Acc)),
Diff1 = Diff + Tdiff,
Sofar1 = Sofar + L0,
Acc1 = Acc#{sofar => Sofar1, ts => TS, diff => Diff1},
check_limit_exceeded(Acc1),
Lines =
case Trace of
{trace_ts, From, call, {Mod, Fun, Args}, TS} ->
{Pid, Node} = pid_and_node(From),
print_call(Fd, Pid, Node, Mod, Fun, Args, Diff1, Acc);
{trace_ts, From, Type, {Mod, Fun, Arity}, Info, TS}
when Type =:= return_from; Type =:= exception_from ->
{Pid, Node} = pid_and_node(From),
print_return(Fd, Type, Pid, Node, Mod, Fun, Arity, Info, Diff1, Acc);
{trace_ts, From, Type, Info, TS} ->
{Pid, Node} = pid_and_node(From),
print_other(Fd, #{type => Type}, Pid, Node, Info, Diff1, Acc);
{trace_ts, From, Type, Arg, Info, TS} ->
{Pid, Node} = pid_and_node(From),
print_other(Fd, #{type => Type, arg => Arg}, Pid, Node, Info, Diff1, Acc);
TraceTS when element(1, TraceTS) == trace_ts ->
fwrite(Fd, "~p~n", [Trace]);
_ ->
fwrite(Fd, "~p~n", [Trace])
end,
Acc1#{sofar => Sofar1 + Lines}.
pid_and_node({Pid, _, Node}) ->
{Pid, Node};
pid_and_node(Pid) when is_pid(Pid); is_port(Pid) ->
{Pid, local}.
fwrite(Fd, Fmt, Args) ->
io_reqs(Fd, [{put_chars, fwrite(Fmt, Args)}]).
io_reqs(Fd, Rs) ->
Lines = count_lines(Rs),
io:requests(Fd, Rs),
Lines.
count_lines(Rs) ->
lists:foldl(
fun(nl, Acc) -> Acc+1;
({put_chars, Cs}, Acc) ->
count_newlines(Cs) + Acc;
({put_chars, unicode, Cs}, Acc) ->
count_newlines(Cs) + Acc
end, 0, Rs).
count_newlines(Cs) ->
case re:run(Cs, <<"\\v">>, [global]) of
{match, Ms} ->
length(Ms);
nomatch ->
0
end.
check_limit_exceeded(#{sofar := Sofar, limit := Limit}) ->
if Sofar > Limit ->
error(limit_exceeded);
true ->
ok
end.
ts(Trace, _TSp) when element(1, Trace) == trace_ts ->
Sz = tuple_size(Trace),
element(Sz, Trace);
ts(_, 0) ->
erlang:timestamp();
ts(_, TSp) ->
TSp.
print_call(Fd, Pid, Node, Mod, Fun, Args, Diff, Acc) ->
case {Fun, Args} of
{event, [Line, Evt, State]} when is_integer(Line) ->
Lines = print_evt(Fd, Pid, Node, Mod, Line, Evt, State, Diff, Acc),
case get_pids({Evt, State}) of
[] -> Lines;
Pids ->
Lines1 = fwrite(Fd, " Nodes = ~p~n", [Pids]),
Lines + Lines1
end;
_ ->
print_call_(Fd, Pid, Node, Mod, Fun, Args, Diff, Acc)
end.
print_return(Fd, Type, Pid, Node, Mod, Fun, Arity, Info, T, Acc) ->
Tstr = fwrite("~w", [T]),
Indent = iolist_size(Tstr) + 3 + 4,
Head = print_return_head(Type, Pid, Node, Mod, Fun, Arity),
Line1Len = iolist_size([Tstr, Head]),
InfoPP = pp(Info, 1, Mod, Acc),
Res = case fits_on_line(InfoPP, Line1Len, 79) of %% minus space
true -> [" ", InfoPP];
false ->
["\n", indent(Indent), pp(Info, Indent, Mod, Acc)]
end,
io_reqs(Fd, [{put_chars, unicode, [Tstr, Head, Res]}, nl]).
print_other(Fd, Type, Pid, Node, Info, T, Acc) ->
Tstr = fwrite("~w", [T]),
Indent = iolist_size(Tstr) + 3 + 4,
Head = print_other_head(Pid, Node, Type),
Line1Len = iolist_size([Tstr, Head]),
Mod = guess_mod(Type, Info),
InfoPP = pp(Info, 1, Mod, Acc),
Res = case fits_on_line(InfoPP, Line1Len, 79) of
true -> [" ", InfoPP];
false ->
["\n", indent(Indent), pp(Info, Indent, Mod, Acc)]
end,
io_reqs(Fd, [{put_chars, unicode, [Tstr, Head, Res]}, nl]).
typestr(return_from ) -> "->";
typestr(exception_from) -> "xx~~>".
guess_mod(Type, Info) ->
guess_mod(Type, Info, ?MODULE).
guess_mod(#{type := exit}, {_, [{Mod, _, _}|_]}, Default) when is_atom(Mod) ->
case erlang:function_exported(Mod, module_info, 0) of
true -> Mod;
false -> Default
end;
guess_mod(_, {erlang, apply, [Mod|_]}, _) when is_atom(Mod) ->
Mod;
guess_mod(_, {erlang, apply, [F|_]}, _) when is_function(F) ->
{_, Mod} = erlang:fun_info(F, module),
Mod;
guess_mod(_, _, Default) ->
Default.
-define(CHAR_MAX, 60).
print_evt(Fd, Pid, N, Mod, L, E, St, T, Acc) ->
Tstr = fwrite("~w", [T]),
Indent = iolist_size(Tstr) + 3,
Head = case N of
local -> fwrite(" - ~w|~w/~w: " , [Pid, Mod, L]);
_ -> fwrite(" - ~w~w|~w/~w: ", [Pid, N, Mod, L])
end,
EvtCol = iolist_size(Head) + 1,
EvtCs = pp(E, EvtCol, Mod, Acc),
io_reqs(Fd, [{put_chars, unicode, [Tstr, Head, EvtCs]}, nl
| print_tail(St, Mod, Indent, Acc)]).
print_call_(Fd, Pid, N, Mod, Fun, Args, T, Acc) ->
Tstr = fwrite("~w", [T]),
Indent = iolist_size(Tstr) + 3 + 4,
Head = print_call_head(Pid, N, Mod, Fun),
Line1Len = iolist_size([Tstr, Head]),
PlainArgs = pp_args(Args, 1, Mod, Acc),
Rest = case fits_on_line(PlainArgs, Line1Len, 79) of %% minus )
true -> [PlainArgs, ")"];
false ->
["\n", indent(Indent),
pp_args(Args, Indent, Mod, Acc), ")"]
end,
io_reqs(Fd, [{put_chars, unicode, [Tstr, Head, Rest]}, nl]).
print_call_head(Pid, N, Mod, Fun) ->
case N of
local -> fwrite(" - ~w|~w:~w(" , [Pid, Mod, Fun]);
_ -> fwrite(" - ~w~w|~w:~w(", [Pid, N, Mod, Fun])
end.
print_other_head(Pid, N, #{type := Type, arg := Arg}) ->
case N of
local -> fwrite(" - ~w|~w[~w]:", [Pid, Type, Arg]);
_ -> fwrite(" - ~w~w|~w[~w]:", [Pid, N, Type, Arg])
end;
print_other_head(Pid, N, #{type := Type}) ->
case N of
local -> fwrite(" - ~w|~w:", [Pid, Type]);
_ -> fwrite(" - ~w~w|~w", [Pid, N, Type])
end.
print_return_head(Type, Pid, Node, Mod, Fun, Arity) ->
case Node of
local -> fwrite(" - ~w|~w:~w/~w " ++ typestr(Type), [Pid, Mod, Fun, Arity]);
_ -> fwrite(" - ~w~w|~w:~w/~w " ++ typestr(Type), [Pid, Node, Mod, Fun, Arity])
end.
fwrite(Fmt, Args) ->
io_lib:fwrite(Fmt, Args).
indent(N) ->
lists:duplicate(N, $\s).
rm_braces(Str) ->
%% allow for whitespace (incl vertical) before and after
B = iolist_to_binary(Str),
Sz = byte_size(B),
{Open,1} = binary:match(B, [<<"{">>]),
{Close,1} = lists:last(
binary:matches(B, [<<"}">>])),
SzA = Open + 1,
SzB = Sz-Close,
SzMid = Sz - SzA - SzB,
<<_:SzA/unit:8,Mid:SzMid/binary,_/binary>> = B,
Mid.
fits_on_line(IOList, Len, LineLen) ->
iolist_size(IOList) + Len =< LineLen
andalso has_no_line_breaks(IOList).
has_no_line_breaks(IOL) ->
nomatch =:= re:run(IOL, <<"\\v">>).
print_tail(none, _, _Col, _Acc) -> [];
print_tail(St, Mod, Col, Acc) ->
Cs = pp(St, Col+1, Mod, Acc),
[{put_chars, unicode, [lists:duplicate(Col,$\s), Cs]}, nl].
%% Pretty-printing the args list as a list, may trigger the 'string' interpretation
%% of the list. We want to remove the brackets anyway, so convert to tuple, then remove the braces
pp_args(Args, Col, Mod, Acc) ->
rm_braces(pp(list_to_tuple(Args), Col, Mod, Acc)).
pp(Term, Col, Mod) ->
pp(Term, Col, Mod, #{}).
pp(Term, Col, Mod, Acc) ->
Out = pp_term(Term, Mod),
io_lib_pretty:print(Out,
[{column, Col},
{line_length, 80},
{depth, -1},
{max_chars, ?CHAR_MAX},
{record_print_fun, record_print_fun(Mod, Acc)}]).
pp_term(T, M) when is_atom(M) ->
pp_term(T, fun M:pp_term/1);
pp_term(T, F) when is_function(F, 1) ->
try F(T) of
{yes, Out} -> Out;
no -> pp_term_(T, F)
catch
error:_ ->
pp_term_(T, F)
end.
pp_term_(D, _) when element(1, D) == dict ->
pp_custom(D, '$dict', fun dict_to_list/1);
pp_term_(T, M) when is_tuple(T) ->
list_to_tuple([pp_term(Trm, M) || Trm <- tuple_to_list(T)]);
pp_term_(L, M) when is_list(L) ->
%% Could be an improper list
lmap(L, fun(T) -> pp_term(T, M) end);
pp_term_(T, _) ->
T.
lmap([H|T], F) when is_list(T) ->
[F(H)|lmap(T,F)];
lmap([H|X], F) ->
[F(H)|F(X)];
lmap([], _) ->
[].
pp_custom(X, Tag, F) ->
try {Tag, F(X)}
catch
error:_ ->
{'ERROR-CUSTOM', Tag, X}
end.
tdiff(_, 0, _) -> 0;
tdiff(TS, T0, Res) ->
case Res of
millisecond ->
timer:now_diff(TS, T0) div 1000;
microsecond ->
timer:now_diff(TS, T0)
end.
time_resolution(Opts) ->
maps:get(time_resolution, Opts, millisecond).
record_print_fun(Mod) ->
record_print_fun(Mod, #{}).
record_print_fun(Mod, Acc) ->
fun(Tag, NoFields) ->
record_print_fun_(Mod, Tag, NoFields, [], Acc)
end.
record_print_fun_(Mod, Tag, NoFields, V, Acc) ->
case shell_record_print(Tag, NoFields, Acc) of
Fields when is_list(Fields) ->
Fields;
_ ->
try Mod:record_fields(Tag) of
Fields when is_list(Fields) ->
case length(Fields) of
NoFields -> Fields;
_ -> no
end;
{check_mods, Mods} when is_list(Mods) ->
check_mods(Mods, Tag, NoFields, V, Acc);
no -> no
catch
_:_ ->
no
end
end.
%% code copied and slightly modified from shell:record_print_fun/1
shell_record_print(Tag, NoFields, #{shell_records_tab := RT}) ->
try ets:lookup(RT, Tag) of
[{_,{attribute,_,record,{Tag,Fields}}}]
when length(Fields) =:= NoFields ->
record_fields(Fields);
_ ->
no
catch
error:_ ->
no
end;
shell_record_print(_, _, _) ->
no.
record_fields([{record_field,_,{atom,_,Field}} | Fs]) ->
[Field | record_fields(Fs)];
record_fields([{record_field,_,{atom,_,Field},_} | Fs]) ->
[Field | record_fields(Fs)];
record_fields([{typed_record_field,Field,_Type} | Fs]) ->
record_fields([Field | Fs]);
record_fields([]) ->
[].
check_mods([], _, _, _, _) ->
no;
check_mods([M|Mods], Tag, NoFields, V, Acc) ->
Cont = fun(V1) -> check_mods(Mods, Tag, NoFields, V1, Acc) end,
case lists:member(M, V) of
true ->
Cont(V);
false ->
V1 = [M|V],
try record_print_fun_(M, Tag, NoFields, V1, Acc) of
Fields when is_list(Fields) ->
Fields;
no ->
Cont(V1)
catch
_:_ ->
Cont(V1)
end
end.
get_pids(Term) ->
Pids = dict:to_list(get_pids(Term, dict:new())),
[{node_prefix(P), N} || {N, P} <- Pids].
get_pids(T, Acc) when is_tuple(T) ->
get_pids(tuple_to_list(T), Acc);
get_pids(L, Acc) when is_list(L) ->
get_pids_(L, Acc);
get_pids(P, Acc) when is_pid(P) ->
try ets:lookup(ttb, P) of
[{_, _, Node}] ->
dict:store(Node, P, Acc);
_ ->
Acc
catch
error:_ -> Acc
end;
get_pids(_, Acc) ->
Acc.
get_pids_([H|T], Acc) ->
get_pids_(T, get_pids(H, Acc));
get_pids_(_, Acc) ->
Acc.
node_prefix(P) ->
case re:run(pid_to_list(P), "[^<\\.]+", [{capture,first,list}]) of
{match, [Pfx]} ->
Pfx;
_ ->
P
end.
cfg(Mod, Key, Default) when is_atom(Mod) ->
try_callback(Mod, Key, [], Default);
cfg(Spec, Key, Default) when is_map(Spec) ->
case maps:find(Key, Spec) of
{ok, Res} -> Res;
error ->
case maps:get(module, Spec, undefined) of
undefined -> Default;
Mod ->
try_callback(Mod, Key, [], Default)
end
end.
try_callback(Mod, Key, Args, Default) ->
ensure_loaded(Mod),
case erlang:function_exported(Mod, Key, length(Args)) of
true ->
apply(Mod, Key, Args);
false ->
Default
end.
ensure_loaded(Mod) ->
case code:ensure_loaded(Mod) of
{module, _} ->
true;
{error, _} ->
false
end.
%% -dialyzer(no_opaque).
-dialyzer([{nowarn_function, dict_to_list/1}, no_opaque]).
dict_to_list(D) when element(1, D) == dict ->
dict:to_list(D).
-dialyzer({nowarn_function, format_time_/1}).
%% ==================================================================
%% Copied from lager_default_formatter.erl, lager_util.erl
format_time(Now) ->
{Date, Time} = format_time_(maybe_utc(localtime_ms(Now))),
["=== Start time: ", Date, " ", Time, " ===\n"].
format_time_({utc, {{Y, M, D}, {H, Mi, S, Ms}}}) ->
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)],
[i2l(H), $:, i2l(Mi), $:, i2l(S), $., i3l(Ms), $ , $U, $T, $C]};
format_time_({{Y, M, D}, {H, Mi, S, Ms}}) ->
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)],
[i2l(H), $:, i2l(Mi), $:, i2l(S), $., i3l(Ms)]};
format_time_({utc, {{Y, M, D}, {H, Mi, S}}}) ->
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)],
[i2l(H), $:, i2l(Mi), $:, i2l(S), $ , $U, $T, $C]};
format_time_({{Y, M, D}, {H, Mi, S}}) ->
{[integer_to_list(Y), $-, i2l(M), $-, i2l(D)],
[i2l(H), $:, i2l(Mi), $:, i2l(S)]}.
i2l(I) when I < 10 -> [$0, $0+I];
i2l(I) -> integer_to_list(I).
i3l(I) when I < 100 -> [$0 | i2l(I)];
i3l(I) -> integer_to_list(I).
localtime_ms(Now) ->
{_, _, Micro} = Now,
{Date, {Hours, Minutes, Seconds}} = calendar:now_to_local_time(Now),
{Date, {Hours, Minutes, Seconds, Micro div 1000 rem 1000}}.
maybe_utc({Date, {H, M, S, Ms}}) ->
case maybe_utc_({Date, {H, M, S}}) of
{utc, {Date1, {H1, M1, S1}}} ->
{utc, {Date1, {H1, M1, S1, Ms}}};
{Date1, {H1, M1, S1}} ->
{Date1, {H1, M1, S1, Ms}}
end.
maybe_utc_(Time) ->
UTC = case application:get_env(sasl, utc_log) of
{ok, Val} ->
Val;
undefined ->
%% Backwards compatible:
application:get_env(stdlib, utc_log, false)
end,
if
UTC =:= true ->
UTCTime = case calendar:local_time_to_universal_time_dst(Time) of
[] -> calendar:local_time();
[T0|_] -> T0
end,
{utc, UTCTime};
true ->
Time
end.
%% ==================================================================