Skip to content

Instantly share code, notes, and snippets.

@seriyps
Last active March 2, 2021 18:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save seriyps/286fc11837ada03262fcb2bd8d7683dc to your computer and use it in GitHub Desktop.
Save seriyps/286fc11837ada03262fcb2bd8d7683dc to your computer and use it in GitHub Desktop.
Small helpers to format common erlang structures (records and stacktraces)
%% @doc Hepers to format common erlang constructs (records, stacktraces) in a more human-readable way
%% Inspired heavily by https://github.com/erlang-lager/lager
-module(log_format).
-export([record/2, f_record/2]).
-export([records/2, f_records/2]).
-export([stack/1, f_stack/1]).
%% @doc Converts record to a string using `#record{}', not `{tuple}' syntax
%%
%% `record(#my_record{a = 1, b = qwerty}, record_info(fields, my_record))'
%%
%% @param Record the record itself
%% @param RecordInfo result of `record_info(fields, record_name)'
-spec record(tuple(), [FieldName :: atom()]) -> string().
record(Record, RecordInfo) ->
lists:flatten(f_record(Record, RecordInfo)).
f_record(Record, RecordInfo) when is_list(RecordInfo),
tuple_size(Record) == length(RecordInfo) + 1 ->
RecordName = element(1, Record),
LookupFun = fun(Name, Size) when Name == RecordName,
Size == length(RecordInfo) ->
RecordInfo;
(_, _) ->
no
end,
io_lib_pretty:print(Record, [ {line_length, 140}
, {record_print_fun, LookupFun}]).
%% @doc same as `record/2', but can format different records, arbitrarily deeply nested
%% ```
%% records(#my_record{a = 1, sub = #my_sub_rec{c = wasd}},
%% #{my_record => record_info(fields, my_record),
%% my_sub_rec => record_info(fields, my_sub_rec)})
%% '''
-spec records(any(), #{atom() => [FieldName :: atom()]}) -> string().
records(Term, RecordInfoMap) ->
lists:flatten(f_records(Term, RecordInfoMap)).
f_records(Term, RecordInfoMap) when is_map(RecordInfoMap) ->
LookupFun = fun(RecordName, NumFields) ->
case RecordInfoMap of
#{RecordName := Fields} when NumFields == length(Fields) ->
Fields;
_ ->
no
end
end,
io_lib_pretty:print(Term, [ {line_length, 140}
, {record_print_fun, LookupFun}]).
-define(MAX_STACKFRAME_LEN, 10240). % soft limit for 1 line
%% @doc Format stacktrace as a human-readable string (pretty-printer)
%%
%% `try erlang:element(10, lists:seq(1, 100)) catch T:R:St -> logger:warning("~p:~p~n~s", [T, R, klog:stack(Stack)]) end.'
%%
%% Strictly one line per stack frame, top of the stack as 1st line
-spec stack([tuple()]) -> string().
stack(Stack) ->
lists:flatten(f_stack(Stack)).
f_stack(Stack) ->
lists:map(
fun(MFA) ->
[" ", f_mfa(MFA), $\n]
end, Stack).
f_mfa({M, F, A}) when is_integer(A) ->
io_lib:format("~w:~w/~b", [M, F, A]);
f_mfa({M, F, Args}) when is_list(Args) ->
Placeholders0 = ["~0tp" || _ <- Args],
Placeholders = lists:join(", ", Placeholders0),
io_lib:format(lists:flatten(["~w:~w(", Placeholders, ")"]),
[M, F | Args],
[{chars_limit, ?MAX_STACKFRAME_LEN}]);
f_mfa({M, F, A, Props}) ->
MFA = f_mfa({M, F, A}),
case {proplists:get_value(file, Props), proplists:get_value(line, Props)} of
{undefined, undefined} when Props == [] ->
MFA;
{undefined, undefined} ->
%% Is it possible?
[MFA | io_lib:format(" extra: ~0p", [Props])];
{undefined, Line} ->
[MFA | io_lib:format(" at line ~b", [Line])];
{File, undefined} ->
%% Is it possible?
[MFA | io_lib:format(" in file ~s", [File])];
{File, Line} ->
[MFA | io_lib:format(" at ~s:~b", [File, Line])]
end;
f_mfa(Other) ->
%% Should never happen?
io_lib:format("~0p", [Other], [{chars_limit, ?MAX_STACKFRAME_LEN}]).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment