defmodule Replbug do
@moduledoc """
Utility for pulling the function call traces into your IEx shell for further analysis and experimentation.
The code is built on top of Rexbug (https://github.com/nietaki/rexbug).
Motivation: Rexbug provides a convenient way of tracing function calls
by printing the trace messages to IEx shell and/or the external file.
In addition, Replbug allows to materialize traces as a variable, and then analyze the call data
in IEx for debugging, experimentation, collecting stats etc.
"""
alias Replbug.Server, as: CollectorServer
require Logger
@spec start(:receive | :send | binary | maybe_improper_list, keyword) ::
:ignore | {:error, any} | {:ok, pid}
def start(trace_pattern, opts \\ []) do
trace_pattern
|> pattern_to_redbug()
# |> add_return_opt()
|> create_call_collector(opts)
end
@spec stop :: %{pid() => list(any())}
@doc """
Stop the collection and get the traces as a map of pid => trace_records
"""
def stop() do
stop(Node.self())
end
def stop(node) do
CollectorServer.stop(node)
end
@spec calls(traces :: %{pid() => Map.t()}) :: %{mfa() => list(any())}
@doc """
Group the trace by function calls (MFA).
"""
def calls(trace, finished \\ true) do
trace
|> Map.values()
|> Enum.map(fn pid_calls ->
(finished && pid_calls.finished_calls) || pid_calls.unfinished_calls
end)
|> List.flatten()
|> Enum.group_by(fn trace_rec ->
{trace_rec.module, trace_rec.function, length(trace_rec.args)}
end)
end
@doc """
Repeat the call with the same arguments.
Use replay/1 with caution in prod!
Also, it may not work as expected due to changes happened in between the initial call and the time of replay.
"""
@spec replay(%{:args => list, :function => atom, :module => atom | tuple, optional(any) => any}) ::
any
def replay(%{function: f, module: m, args: a, caller_pid: pid} = _call_record, timeout \\ 5_000) do
caller_node = node(pid)
if caller_node == Node.self() do
apply(m, f, a)
else
:erpc.call(caller_node, m, f, a, timeout)
end
end
defp create_call_collector(call_pattern, opts) do
CollectorServer.start(call_pattern, opts)
end
## Tracing for messages
defp pattern_to_redbug(trace_pattern) when trace_pattern in [:send, :receive] do
trace_pattern
end
defp pattern_to_redbug({m, f, a}) do
"#{m}.#{f}/#{a}"
|> String.replace("Elixir.", "")
|> pattern_to_redbug()
end
defp pattern_to_redbug(function) when is_function(function) do
function
|> Function.info()
|> then(fn info ->
(info[:type] == :external && pattern_to_redbug({info[:module], info[:name], info[:arity]})) ||
throw({:error, :local_functions_not_supported})
end)
end
defp pattern_to_redbug(module) when is_atom(module) do
"#{module}"
|> String.replace("Elixir.", "")
|> pattern_to_redbug()
end
## Tracing for fun calls
defp pattern_to_redbug(trace_pattern) when is_binary(trace_pattern) do
## Force `return` option
case String.split(trace_pattern, ~r{::}, trim: true, include_captures: true) do
[no_opts_call] ->
"#{no_opts_call} :: return"
[call, "::", opts] ->
(String.contains?(opts, "return") && trace_pattern) ||
"#{call} :: return,#{String.trim(opts)}"
end
end
defp pattern_to_redbug(call_pattern_list) when is_list(call_pattern_list) do
Enum.map(call_pattern_list, fn pattern -> pattern_to_redbug(pattern) end)
end
end