Current section
Files
Jump to
Current section
Files
lib/entrace.ex
defmodule Entrace do
@moduledoc """
Base module for starting a GenServer that manages trace sessions.
Typically you would rather add a module to your project that uses the `Entrace.Tracer`.
```elixir
defmodule MyApp.Tracer do
use Entrace.Tracer
end
```
And in your application.ex list of children to supervise add:
```
MyApp.Tracer
```
It will run as locally registered on it's module name (in this case `MyApp.Tracer`).
Each call to `trace/4` creates an isolated OTP 27+ trace session with its own
worker process. Multiple traces operate independently without interfering with
each other or with other tracing tools on the node.
Requires OTP 27 or later.
"""
use GenServer
alias Entrace.TraceWorker
require Logger
@default_limit 200
@big_limit 10_000
@type tracer() :: GenServer.server()
@type mfa_pattern() :: {atom(), atom(), atom() | non_neg_integer()}
@type transmission() :: function() | pid() | mfa()
@type trace_result() :: {:set, non_neg_integer()} | {:reset_existing, non_neg_integer()}
@type trace_error() :: :full_wildcard_rejected
@doc """
Starts the Tracer linked to the parent process.
Operations can then be performed on the resulting pid or registered name.
Options:
* `:session_prefix` - atom prefix for trace session names (default: `:entrace`)
* All other options are passed to `GenServer.start_link/3`.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
{session_prefix, opts} = Keyword.pop(opts, :session_prefix, :entrace)
GenServer.start_link(__MODULE__, session_prefix, opts)
end
@doc """
Start a trace for a provided `pattern` and `transmission` mechanism.
The `pattern` is an mfa tuple, as in `{module, function, arity}`.
For example `{File, read, 1}` to trace the `File.read/1` function. It
supports a special underscore atom `:_` to indicate a wildcard in the mfa.
You can only use wildcards on the function and arity and Erlang's tracing
only allows wildcard function if the arity is also a wildcard.
The `transmission` is one of:
* a `PID` to send traces to.
* a callback function.
* a callback function provided as `{module, function, arity}`
Optional options are available. The options are:
* `limit` - the maximum number of messages to process before ending the trace
on that pattern. Default limit is 200.
* `time_limit` - time to leave the trace running. If no `limit` is specified
this will use a larger default limit of 10_000.
Returns information about how setting the trace pattern worked out.
"""
@spec trace(
tracer :: tracer(),
mfa :: mfa_pattern(),
transmission :: transmission(),
opts :: keyword()
) :: {:ok, trace_result()} | {:error, trace_error()}
def trace(tracer, mfa, transmission, opts \\ [])
def trace(tracer, {m, f, a} = mfa, callback, opts)
when is_atom(m) and is_atom(f) and (is_integer(a) or a == :_) and is_function(callback) do
GenServer.call(tracer, {:start_trace, mfa, callback, opts})
end
def trace(tracer, {m, f, a} = mfa, {m2, f2, 1} = callback_mfa, opts)
when is_atom(m) and is_atom(f) and (is_integer(a) or a == :_) and is_atom(m2) and
is_atom(f2) do
GenServer.call(tracer, {:start_trace, mfa, callback_mfa, opts})
end
def trace(tracer, {m, f, a} = mfa, recipient_pid, opts)
when is_atom(m) and is_atom(f) and (is_integer(a) or a == :_) and is_pid(recipient_pid) do
GenServer.call(tracer, {:start_trace, mfa, recipient_pid, opts})
end
@doc """
Start a trace across the entire cluster.
See `Entrace.trace/4` for details on arguments.
"""
@spec trace_cluster(
tracer :: tracer(),
mfa :: mfa_pattern(),
transmission :: transmission(),
opts :: keyword()
) :: [{:ok, trace_result()} | {:error, trace_error()}]
def trace_cluster(tracer, mfa, transmission, opts \\ []) do
GenServer.call(tracer, {:trace_cluster, mfa, transmission, opts})
end
@doc """
Stop tracing the `mfa` pattern.
"""
@spec stop(tracer :: tracer(), mfa :: mfa_pattern()) :: :ok
def stop(tracer, {m, f, a} = mfa)
when is_atom(m) and is_atom(f) and (is_integer(a) or a == :_) do
GenServer.call(tracer, {:stop_trace, mfa})
end
@doc false
@spec exit(pid()) :: true
def exit(pid) do
Process.exit(pid, :user_halt)
end
@doc """
Get a map of trace info for function calls based on patterns.
The map is keyed by mfas that have been seen during the trace.
Trace info includes information from `:trace.info/3`.
It includes `:call_count`, `:call_memory` and `:call_time`.
"""
@spec list_trace_info(tracer :: tracer()) :: map()
def list_trace_info(tracer) do
GenServer.call(tracer, :list_trace_info)
end
@doc """
Get map of active trace patterns.
"""
@spec list_trace_patterns(tracer :: tracer()) :: map()
def list_trace_patterns(tracer) do
GenServer.call(tracer, :list_trace_patterns)
end
# GenServer callbacks
@impl GenServer
def init(session_prefix) do
Logger.debug("Starting Entrace GenServer...")
:pg.join(Entrace.Tracing, Entrace.Tracers, self())
state = %{
traces: %{},
counter: 0,
session_prefix: session_prefix
}
{:ok, state}
end
@impl GenServer
def handle_call({:start_trace, {:_, :_, :_}, _, _}, _from, state) do
{:reply, {:error, :full_wildcard_rejected}, state}
end
def handle_call({:start_trace, mfa, transmission, opts}, _from, state)
when is_pid(transmission) or is_function(transmission) or is_tuple(transmission) do
{result, state} = start_trace_worker(mfa, transmission, opts, state)
{:reply, result, state}
end
def handle_call({:trace_cluster, {:_, :_, :_}, _, _}, _from, state) do
{:reply, {:error, :full_wildcard_rejected}, state}
end
def handle_call({:trace_cluster, mfa, transmission, opts}, _from, state)
when is_pid(transmission) or is_function(transmission) or is_tuple(transmission) do
# Start local trace
{{:ok, _} = result, state} = start_trace_worker(mfa, transmission, opts, state)
# Remote traces send back to local worker
local_worker = state.traces[mfa]
results =
:pg.get_members(Entrace.Tracing, Entrace.Tracers)
|> Enum.reject(&(&1 == self()))
|> Enum.map(fn pid ->
Entrace.trace(pid, mfa, local_worker, opts)
end)
{:reply, [result | results], state}
end
def handle_call({:stop_trace, mfa}, _from, state) do
state = stop_worker(mfa, state)
{:reply, :ok, state}
end
def handle_call(:list_trace_info, _from, state) do
infos =
state.traces
|> Enum.flat_map(fn {_mfa, worker_pid} ->
try do
TraceWorker.get_trace_info(worker_pid)
catch
:exit, _ -> []
end
end)
|> Map.new()
{:reply, infos, state}
end
def handle_call(:list_trace_patterns, _from, state) do
{:reply, state.traces, state}
end
@impl GenServer
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
traces =
state.traces
|> Enum.reject(fn {_mfa, worker} -> worker == pid end)
|> Map.new()
{:noreply, %{state | traces: traces}}
end
def handle_info(other, state) do
Logger.error("Got unexpected message: #{inspect(other)}")
{:noreply, state}
end
@impl GenServer
def terminate(_reason, state) do
Logger.debug("Shutting down, stopping all trace workers")
Enum.each(state.traces, fn {_mfa, worker_pid} ->
safe_stop_worker(worker_pid)
end)
end
# Private helpers
defp start_trace_worker(mfa, transmission, opts, state) do
limit = compute_limit(opts)
{session_name, counter} = next_session_name(state)
{was_reset, state} = maybe_stop_existing(mfa, state)
{:ok, worker_pid} =
TraceWorker.start_link(%{
session_name: session_name,
mfa: mfa,
transmission: transmission,
limit: limit,
time_limit: opts[:time_limit]
})
Process.monitor(worker_pid)
matched = TraceWorker.get_matched_count(worker_pid)
result_type = if was_reset, do: :reset_existing, else: :set
traces = Map.put(state.traces, mfa, worker_pid)
{{:ok, {result_type, matched}}, %{state | traces: traces, counter: counter}}
end
defp compute_limit(opts) do
if opts[:time_limit] do
opts[:limit] || @big_limit
else
opts[:limit] || @default_limit
end
end
defp next_session_name(state) do
counter = state.counter + 1
name = :"#{state.session_prefix}_#{counter}"
{name, counter}
end
defp maybe_stop_existing(mfa, state) do
case Map.pop(state.traces, mfa) do
{nil, _} ->
{false, state}
{worker_pid, traces} ->
safe_stop_worker(worker_pid)
{true, %{state | traces: traces}}
end
end
defp stop_worker(mfa, state) do
case Map.pop(state.traces, mfa) do
{nil, traces} ->
%{state | traces: traces}
{worker_pid, traces} ->
safe_stop_worker(worker_pid)
%{state | traces: traces}
end
end
defp safe_stop_worker(worker_pid) do
if Process.alive?(worker_pid) do
try do
TraceWorker.stop(worker_pid)
catch
:exit, _ -> :ok
end
end
end
end