Merge pull request #74 from gomoripeti/trace_fmt_extra_msg

Support extra message in traces
diff --git a/.travis.yml b/.travis.yml
index 715a9e0..47900b0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,7 @@
 sudo: false
 language: erlang
 otp_release:
+  - 22.0
   - 21.0
   - 20.0
   - 19.3
diff --git a/README.md b/README.md
index 5463862..5a11f73 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,9 @@
 
 *2.x*
 
+- 2.5.0
+  - Optional formatting of records in traces (thanks to @bartekgorny)
+  - Basic support for OTP-22 in `recon_alloc` (base handling of `foreign_blocks` type)
 - 2.4.0
   - Optional formatting of records in traces (thanks to @bartekgorny)
 - 2.3.6
diff --git a/doc/overview.edoc b/doc/overview.edoc
index ed86d63..b5c2250 100644
--- a/doc/overview.edoc
+++ b/doc/overview.edoc
@@ -1,7 +1,7 @@
 @title Recon Application
 @author Fred Hebert <mononcqc@ferd.ca>
   [http://ferd.ca/]
-@copyright 2012-2018 Fred Hebert (BSD 3-Clause License)
+@copyright 2012-2019 Fred Hebert (BSD 3-Clause License)
 
 @doc Recon is a library to be dropped into any other Erlang project,
 to be used to assist DevOps people diagnose problems in production
diff --git a/mix.exs b/mix.exs
new file mode 100644
index 0000000..0580395
--- /dev/null
+++ b/mix.exs
@@ -0,0 +1,13 @@
+defmodule Recon.MixProject do
+  use Mix.Project
+
+  def project do
+    [
+      app: :recon,
+      description: "Diagnostic tools for production use",
+      version: "2.5.0",
+      language: :erlang,
+      deps: []
+    ]
+  end
+end
diff --git a/src/recon.app.src b/src/recon.app.src
index fbeb72a..38a2e91 100644
--- a/src/recon.app.src
+++ b/src/recon.app.src
@@ -1,13 +1,13 @@
 {application, recon,
  [{description, "Diagnostic tools for production use"},
-  {vsn, "2.4.0"},
+  {vsn, "2.5.0"},
   {modules, [recon, recon_alloc, recon_lib, recon_trace, recon_rec]},
   {registered, []},
   {applications, [kernel, stdlib]},
 
-  {maintainers, ["Fred Hebert"]},
   {licenses, ["BSD"]},
   {links, [{"Github", "https://github.com/ferd/recon/"},
            {"Documentation", "http://ferd.github.io/recon/"}]},
-  {files, ["src/", "script/", "rebar.lock", "README.md", "LICENSE"]}
+  {build_tools, ["mix", "rebar3"]},
+  {files, ["src/", "script/", "rebar.lock", "mix.exs", "README.md", "LICENSE"]}
 ]}.
diff --git a/src/recon_alloc.erl b/src/recon_alloc.erl
index 7c2d029..a89354d 100644
--- a/src/recon_alloc.erl
+++ b/src/recon_alloc.erl
@@ -420,6 +420,9 @@
                    %% value is very rarely important so leave it
                    %% like this for now.
                    {K, lists:max([V1,V2])};
+              ({{K,V1}, {K,V2}}) when K =:= foreign_blocks ->
+                   %% foreign blocks are just merged as a bigger list.
+                   {K, V1++V2};
               ({{K,V1}, {K,V2}}) ->
                    {K, V1 + V2};
               ({{K,C1,L1,M1}, {K,C2,L2,M2}}) ->
diff --git a/src/recon_map.erl b/src/recon_map.erl
new file mode 100644
index 0000000..f80e587
--- /dev/null
+++ b/src/recon_map.erl
@@ -0,0 +1,208 @@
+%%%-------------------------------------------------------------------
+%%% @author bartlomiej.gorny@erlang-solutions.com
+%%% @doc
+%%% This module handles formatting maps.
+%% It allows for trimming output to selected fields, or to nothing at all. It also adds a label
+%% to a printout.
+%% To set up a limit for a map, you need to give recon a way to tell the map you want to
+%% trim from all the other maps, so you have to provide something like a 'type definition'.
+%% It can be either another map which is compared to the arg, or a fun.
+%%% @end
+%%%-------------------------------------------------------------------
+-module(recon_map).
+-author("bartlomiej.gorny@erlang-solutions.com").
+%% API
+
+-export([limit/3, list/0, is_active/0, clear/0, remove/1, rename/2]).
+-export([process_map/1]).
+
+-type map_label() :: atom().
+-type pattern() :: map() | function().
+-type limit() :: all | none | atom() | binary() | [any()].
+
+%% @doc quickly check if we want to do any record formatting
+-spec is_active() -> boolean().
+is_active() ->
+    case whereis(recon_ets_maps) of
+        undefined -> false;
+        _ -> true
+    end.
+
+%% @doc remove all imported definitions, destroy the table, clean up
+clear() ->
+    maybe_kill(recon_ets_maps),
+    ok.
+
+%% @doc Limit output to selected keys of a map (can be 'none', 'all', a key or a list of keys).
+%% Pattern selects maps to process: a "pattern" is just a map, and if all key/value pairs of a pattern
+%% are present in a map (in other words, the pattern is a subset), then we say the map matches
+%% and we process it accordingly (apply the limit).
+%%
+%% Patterns are applied in alphabetical order, until a match is found.
+%%
+%% Instead of a pattern you can also provide a function which will take a map and return a boolean.
+%% @end
+-spec limit(map_label(), pattern(), limit()) -> ok | {error, any()}.
+limit(Label, #{} = Pattern, Limit) when is_atom(Label) ->
+    store_pattern(Label, Pattern, Limit);
+limit(Label, Pattern, Limit) when is_atom(Label), is_function(Pattern) ->
+    store_pattern(Label, Pattern, Limit).
+
+%% @doc prints out all "known" map definitions and their limit settings.
+%% Printout tells a map's name, the matching fields required, and the limit options.
+%% @end
+list() ->
+    ensure_table_exists(),
+    io:format("~nmap definitions and limits:~n"),
+    list(ets:tab2list(patterns_table_name())).
+
+%% @doc remove a given map entry
+-spec remove(map_label()) -> true.
+remove(Label) ->
+    ensure_table_exists(),
+    ets:delete(patterns_table_name(), Label).
+
+%% @doc rename a given map entry, which allows to to change priorities for
+%% matching. The first argument is the current name, and the second
+%% argument is the new name.
+-spec rename(map_label(), map_label()) -> renamed | missing.
+rename(Name, NewName) ->
+    ensure_table_exists(),
+    case ets:lookup(patterns_table_name(), Name) of
+        [{Name, Pattern, Limit}] ->
+            ets:insert(patterns_table_name(), {NewName, Pattern, Limit}),
+            ets:delete(patterns_table_name(), Name),
+            renamed;
+        [] ->
+            missing
+    end.
+
+%% @doc prints out all "known" map filter definitions and their settings.
+%% Printout tells the map's label, the matching patterns, and the limit options
+%% @end
+list([]) ->
+    io:format("~n"),
+    ok;
+list([{Label, Pattern, Limit} | Rest]) ->
+    io:format("~p: ~p -> ~p~n", [Label, Pattern, Limit]),
+    list(Rest).
+
+%% @private given a map, scans saved patterns for one that matches; if found, returns a label
+%% and a map with limits applied; otherwise returns 'none' and original map.
+%% Pattern can be:
+%% <ul>
+%% <li> a map - then each key in pattern is checked for equality with the map in question</li>
+%% <li> a fun(map()) -> boolean()</li>
+%% </ul>
+-spec process_map(map()) -> map() | {atom(), map()}.
+process_map(M) ->
+    process_map(M, ets:tab2list(patterns_table_name())).
+
+process_map(M, []) ->
+    M;
+process_map(M, [{Label, Pattern, Limit} | Rest]) ->
+    case map_matches(M, Pattern) of
+        true ->
+            {Label, apply_map_limits(Limit, M)};
+        false ->
+            process_map(M, Rest)
+    end.
+
+map_matches(#{} = M, Pattern) when is_function(Pattern) ->
+    Pattern(M);
+map_matches(_, []) ->
+    true;
+map_matches(M, [{K, V} | Rest]) ->
+    case maps:is_key(K, M) of
+        true ->
+            case maps:get(K, M) of
+                V ->
+                    map_matches(M, Rest);
+                _ ->
+                    false
+            end;
+        false ->
+            false
+    end.
+
+apply_map_limits(none, M) ->
+    M;
+apply_map_limits(all, _) ->
+    #{};
+apply_map_limits(Fields, M) ->
+    maps:with(Fields, M).
+
+patterns_table_name() -> recon_map_patterns.
+
+store_pattern(Label, Pattern, Limit) ->
+    ensure_table_exists(),
+    ets:insert(patterns_table_name(), {Label, prepare_pattern(Pattern), prepare_limit(Limit)}),
+    ok.
+
+prepare_limit(all) -> all;
+prepare_limit(none) -> none;
+prepare_limit(Limit) when is_binary(Limit) -> [Limit];
+prepare_limit(Limit) when is_atom(Limit) -> [Limit];
+prepare_limit(Limit) when is_list(Limit) -> Limit.
+
+prepare_pattern(Pattern) when is_function(Pattern) -> Pattern;
+prepare_pattern(Pattern) when is_map(Pattern) -> maps:to_list(Pattern).
+
+
+ensure_table_exists() ->
+    case ets:info(patterns_table_name()) of
+        undefined ->
+            case whereis(recon_ets_maps) of
+                undefined ->
+                    Parent = self(),
+                    Ref = make_ref(),
+                    %% attach to the currently running session
+                    {Pid, MonRef} = spawn_monitor(fun() ->
+                        register(recon_ets_maps, self()),
+                        ets:new(patterns_table_name(), [ordered_set, public, named_table]),
+                        Parent ! Ref,
+                        ets_keeper()
+                    end),
+                    receive
+                        Ref ->
+                            erlang:demonitor(MonRef, [flush]),
+                            Pid;
+                        {'DOWN', MonRef, _, _, Reason} ->
+                            error(Reason)
+                    end;
+                Pid ->
+                    Pid
+            end;
+        Pid ->
+            Pid
+    end.
+
+ets_keeper() ->
+    receive
+        stop -> ok;
+        _ -> ets_keeper()
+    end.
+
+%%%%%%%%%%%%%%%
+%%% HELPERS %%%
+%%%%%%%%%%%%%%%
+
+maybe_kill(Name) ->
+    case whereis(Name) of
+        undefined ->
+            ok;
+        Pid ->
+            unlink(Pid),
+            exit(Pid, kill),
+            wait_for_death(Pid, Name)
+    end.
+
+wait_for_death(Pid, Name) ->
+    case is_process_alive(Pid) orelse whereis(Name) =:= Pid of
+        true ->
+            timer:sleep(10),
+            wait_for_death(Pid, Name);
+        false ->
+            ok
+    end.
+
diff --git a/src/recon_rec.erl b/src/recon_rec.erl
index fb31556..3689df1 100644
--- a/src/recon_rec.erl
+++ b/src/recon_rec.erl
@@ -15,7 +15,8 @@
 %% API
 
 -export([is_active/0]).
--export([import/1, format_tuple/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
+-export([import/1, clear/1, clear/0, list/0, get_list/0, limit/3]).
+-export([format_tuple/1]).
 
 -ifdef(TEST).
 -export([lookup_record/2]).
@@ -54,7 +55,7 @@
 
 %% @doc remove definitions imported from a module.
 clear(Module) ->
-    lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(ets_table_name())).
+    lists:map(fun(R) -> rem_for_module(R, Module) end, ets:tab2list(records_table_name())).
 
 %% @doc remove all imported definitions, destroy the table, clean up
 clear() ->
@@ -62,7 +63,7 @@
     ok.
 
 %% @doc prints out all "known" (imported) record definitions and their limit settings.
-%% Print out tells module a record originates from, its name and a list of field names,
+%% Printout tells module a record originates from, its name and a list of field names,
 %% plus the record's arity (may be handy if handling big records) and a list of field it
 %% limits its output to, if set.
 %% @end
@@ -80,20 +81,20 @@
 -spec get_list() -> [listentry()].
 get_list() ->
     ensure_table_exists(),
-    Lst = lists:map(fun make_list_entry/1, ets:tab2list(ets_table_name())),
+    Lst = lists:map(fun make_list_entry/1, ets:tab2list(records_table_name())),
     lists:sort(Lst).
 
 %% @doc Limit output to selected fields of a record (can be 'none', 'all', a field or a list of fields).
 %% Limit set to 'none' means there is no limit, and all fields are displayed; limit 'all' means that
 %% all fields are squashed and only record name will be shown.
 %% @end
--spec limit(record_name(), arity(), limit()) -> ok | {error, record_unknown}.
-limit(Name, Arity, Limit) ->
+-spec limit(record_name(), arity(), limit()) -> ok | {error, any()}.
+limit(Name, Arity, Limit) when is_atom(Name), is_integer(Arity) ->
     case lookup_record(Name, Arity) of
         [] ->
             {error, record_unknown};
         [{Key, Fields, Mod, _}] ->
-            ets:insert(ets_table_name(), {Key, Fields, Mod, Limit}),
+            ets:insert(records_table_name(), {Key, Fields, Mod, Limit}),
             ok
     end.
 
@@ -127,10 +128,10 @@
     Arity = length(Fields),
     Result = case lookup_record(Name, Arity) of
         [] ->
-            ets:insert(ets_table_name(), rec_info(Rec, Module)),
+            ets:insert(records_table_name(), rec_info(Rec, Module)),
             {imported, Module, Name, Arity};
         [{_, _, Module, _}] ->
-            ets:insert(ets_table_name(), rec_info(Rec, Module)),
+            ets:insert(records_table_name(), rec_info(Rec, Module)),
             {overwritten, Module, Name, Arity};
         [{_, _, Mod, _}] ->
             {ignored, Module, Name, Arity, Mod}
@@ -148,10 +149,11 @@
 %% @private
 lookup_record(RecName, FieldCount) ->
     ensure_table_exists(),
-    ets:lookup(ets_table_name(), {RecName, FieldCount}).
+    ets:lookup(records_table_name(), {RecName, FieldCount}).
 
+%% @private
 ensure_table_exists() ->
-    case ets:info(ets_table_name()) of
+    case ets:info(records_table_name()) of
         undefined ->
             case whereis(recon_ets) of
                 undefined ->
@@ -160,7 +162,7 @@
                     %% attach to the currently running session
                     {Pid, MonRef} = spawn_monitor(fun() ->
                         register(recon_ets, self()),
-                        ets:new(ets_table_name(), [set, public, named_table]),
+                        ets:new(records_table_name(), [set, public, named_table]),
                         Parent ! Ref,
                         ets_keeper()
                     end),
@@ -178,13 +180,13 @@
             Pid
     end.
 
-ets_table_name() -> recon_record_definitions.
+records_table_name() -> recon_record_definitions.
 
 rec_info({Name, Fields}, Module) ->
     {{Name, length(Fields)}, field_names(Fields), Module, none}.
 
 rem_for_module({_, _, Module, _} = Rec, Module) ->
-    ets:delete_object(ets_table_name(), Rec);
+    ets:delete_object(records_table_name(), Rec);
 rem_for_module(_, _) ->
     ok.
 
@@ -233,6 +235,7 @@
     end.
 
 format_kv(Key, Val) ->
+    %% Some messy mutually recursive calls we can't avoid
     [recon_trace:format_trace_output(true, Key), "=", recon_trace:format_trace_output(true, Val)].
 
 apply_limits(List, none) -> List;
diff --git a/src/recon_trace.erl b/src/recon_trace.erl
index 53e851f..6cd65f2 100644
--- a/src/recon_trace.erl
+++ b/src/recon_trace.erl
@@ -185,7 +185,7 @@
 -export([format/1]).
 
 %% Internal exports
--export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/2]).
+-export([count_tracer/1, rate_tracer/2, formatter/5, format_trace_output/1, format_trace_output/2]).
 
 -type matchspec()    :: [{[term()], [term()], [term()]}].
 -type shellfun()     :: fun((_) -> term()).
@@ -610,36 +610,76 @@
 format_args(Arity) when is_integer(Arity) ->
     [$/, integer_to_list(Arity)];
 format_args(Args) when is_list(Args) ->
-    Active = recon_rec:is_active(),
-    [$(, join(", ", [format_trace_output(Active, Arg) || Arg <- Args]), $)].
+    [$(, join(", ", [format_trace_output(Arg) || Arg <- Args]), $)].
 
 
 %% @doc formats call arguments and return values - most types are just printed out, except for
 %% tuples recognised as records, which mimic the source code syntax
 %% @end
 format_trace_output(Args) ->
-    format_trace_output(recon_rec:is_active(), Args).
+    format_trace_output(recon_rec:is_active(), recon_map:is_active(), Args).
 
-format_trace_output(true, Args) when is_tuple(Args) ->
+format_trace_output(Recs, Args) ->
+    format_trace_output(Recs, recon_map:is_active(), Args).
+
+format_trace_output(true, _, Args) when is_tuple(Args) ->
     recon_rec:format_tuple(Args);
-format_trace_output(true, Args) when is_list(Args) ->
+format_trace_output(false, true, Args) when is_tuple(Args) ->
+    format_tuple(false, true, Args);
+format_trace_output(Recs, Maps, Args) when is_list(Args), Recs orelse Maps ->
     case io_lib:printable_list(Args) of
         true ->
             io_lib:format("~p", [Args]);
         false ->
-            L = lists:map(fun(A) -> format_trace_output(true, A) end, Args),
-            [$[, join(", ", L), $]]
+            format_maybe_improper_list(Recs, Maps, Args)
     end;
-format_trace_output(true, Args) when is_map(Args) ->
+format_trace_output(Recs, true, Args) when is_map(Args) ->
+    {Label, Map} = case recon_map:process_map(Args) of
+                       {L, M} -> {atom_to_list(L), M};
+                       M -> {"", M}
+                   end,
+    ItemList = maps:to_list(Map),
+    [Label,
+     "#{",
+        join(", ", [format_kv(Recs, true, Key, Val) || {Key, Val} <- ItemList]),
+    "}"];
+format_trace_output(Recs, false, Args) when is_map(Args) ->
     ItemList = maps:to_list(Args),
     ["#{",
-        join(", ", [format_kv(Key, Val) || {Key, Val} <- ItemList]),
+        join(", ", [format_kv(Recs, false, Key, Val) || {Key, Val} <- ItemList]),
     "}"];
-format_trace_output(_, Args) ->
+format_trace_output(_, _, Args) ->
     io_lib:format("~p", [Args]).
 
-format_kv(Key, Val) ->
-    [format_trace_output(true, Key), "=", format_trace_output(true, Val)].
+format_kv(Recs, Maps, Key, Val) ->
+    [format_trace_output(Recs, Maps, Key), "=>", format_trace_output(Recs, Maps, Val)].
+
+
+format_tuple(Recs, Maps, Tup) ->
+    [${ | format_tuple_(Recs, Maps, tuple_to_list(Tup))].
+
+format_tuple_(_Recs, _Maps, []) ->
+    "}";
+format_tuple_(Recs, Maps, [H|T]) ->
+    [format_trace_output(Recs, Maps, H), $,,
+     format_tuple_(Recs, Maps, T)].
+
+
+format_maybe_improper_list(Recs, Maps, List) ->
+    [$[ | format_maybe_improper_list_(Recs, Maps, List)].
+
+format_maybe_improper_list_(_, _, []) ->
+    "]";
+format_maybe_improper_list_(Recs, Maps, [H|[]]) ->
+    [format_trace_output(Recs, Maps, H), $]];
+format_maybe_improper_list_(Recs, Maps, [H|T]) when is_list(T) ->
+    [format_trace_output(Recs, Maps, H), $,,
+     format_maybe_improper_list_(Recs, Maps, T)];
+format_maybe_improper_list_(Recs, Maps, [H|T]) when not is_list(T) ->
+    %% Handling improper lists
+    [format_trace_output(Recs, Maps, H), $|,
+     format_trace_output(Recs, Maps, T), $]].
+
 
 %%%%%%%%%%%%%%%
 %%% HELPERS %%%
@@ -681,6 +721,7 @@
             exit(shell_funs_only)
     end.
 
+
 -ifdef(OTP_RELEASE).
 -spec join(term(), [term()]) -> [term()].
 join(Sep, List) ->
diff --git a/test/recon_SUITE.erl b/test/recon_SUITE.erl
index 6cf63fb..dbb4d86 100644
--- a/test/recon_SUITE.erl
+++ b/test/recon_SUITE.erl
@@ -66,7 +66,7 @@
 
 info4(Config) ->
     Pid = ?config(pid, Config),
-    Keys = [meta, signals, location, memory_used, work,
+    Keys = [meta, signals, location, memory_used,
             links, monitors, messages,
             [links, monitors, messages]],
     {A,B,C} = pid_to_triple(Pid),