Current section

Files

Jump to
grizzly lib grizzly trace.ex
Raw

lib/grizzly/trace.ex

defmodule Grizzly.Trace do
@moduledoc """
Module that tracks the commands that are sent and received by Grizzly
The trace will hold in memory the last 300 messages. If you want to generate
a log file of the trace records you use `Grizzly.Trace.dump/1`.
The log format is:
```
timestamp source destination sequence_number command_name command_parameters
```
If you want to list the records that are currently being held in memory you
can use `Grizzly.Trace.list/0`.
If you want to start traces from a fresh start you can call
`Grizzly.Trace.clear/0`.
"""
use GenServer
alias Grizzly.Trace.Record
@type trace_opt :: {:name, atom()} | {:size, pos_integer()} | {:record_keepalives, boolean()}
@type src() :: Grizzly.node_id() | :grizzly
@type dest() :: Grizzly.node_id() | :grizzly
@type log_opt() :: {:src, src()} | {:dest, dest()}
@type format() :: :text | :raw
@type list_opt() :: {:node_id, Grizzly.node_id()}
@default_size 300
@default_format :text
@doc "Start the trace server."
@spec start_link([trace_opt()]) :: GenServer.on_start()
def start_link(opts \\ []) do
{name, opts} = Keyword.pop(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end
@doc """
Serialize a list of trace records into a binary. The format can be one of:
* `:text` - Each record is on a new line and is
formatted as `timestamp source destination sequence_number command_name
command_binary`. This is the default format.
* `:raw` - Each record is on a new line and is formatted as
`timestamp source -> destination: binary`.
"""
@spec format([Record.t()], format()) :: binary()
def format(records, format \\ @default_format)
def format(records, fmt) when fmt in [:text, :raw],
do: Enum.map_join(records, "\n", &Record.to_string(&1, fmt))
@doc "Dump trace records into a file using Erlang External Term Format."
@spec dump(binary()) :: :ok | {:error, atom()}
def dump(file) do
file_contents = :erlang.term_to_binary(list(), compressed: 9)
File.write(file, file_contents)
end
@doc """
Write trace records to standard out. See `format/2` for the available formats.
"""
@spec print(list(Record.t()) | format(), [list_opt()]) :: :ok
def print(records_or_format, opts)
def print(records, _opts) when is_list(records), do: records |> format() |> IO.puts()
def print(fmt, opts) when is_atom(fmt), do: opts |> list() |> format(fmt) |> IO.puts()
def print(fmt_or_opts)
def print(fmt) when is_atom(fmt), do: print(fmt, [])
def print(opts) when is_list(opts), do: print(@default_format, opts)
@spec print() :: :ok
def print(), do: print([])
@doc "Add a record to the trace buffer."
@spec log(GenServer.server(), binary(), [log_opt()]) :: :ok
def log(name, binary, opts) do
GenServer.cast(name, {:log, binary, opts})
end
@doc "See `log/3`"
@spec log(binary(), [log_opt()]) :: :ok
def log(binary, opts \\ []) when is_binary(binary), do: log(__MODULE__, binary, opts)
@doc "Reset the trace buffer."
@spec clear(GenServer.server()) :: :ok
def clear(name \\ __MODULE__), do: GenServer.call(name, :clear)
@doc "List all records in the trace buffer."
@spec list(GenServer.server(), [list_opt()]) :: [Record.t()]
def list(name, opts) do
node_id_filter = Keyword.get(opts, :node_id, nil)
records = GenServer.call(name, :list)
if node_id_filter do
Enum.filter(records, &(&1.src == node_id_filter || &1.dest == node_id_filter))
else
records
end
end
@doc "See `list/2`."
@spec list(GenServer.server() | [list_opt()]) :: [Record.t()]
def list(opts) when is_list(opts), do: list(__MODULE__, opts)
def list(name), do: list(name, [])
@doc "See `list/2`."
@spec list() :: [Record.t()]
def list(), do: list(__MODULE__, [])
@doc "Change the max size of the trace buffer from the default of #{@default_size}."
@spec resize(GenServer.server(), pos_integer()) :: :ok
def resize(name \\ __MODULE__, size), do: GenServer.call(name, {:resize, size})
@doc "Enable or disable logging of keepalive frames."
@spec record_keepalives(GenServer.server(), boolean()) :: :ok
def record_keepalives(name, enabled?), do: GenServer.call(name, {:record_keepalives, enabled?})
@doc "See `record_keepalives/2`."
@spec record_keepalives(boolean()) :: :ok
def record_keepalives(enabled? \\ true), do: record_keepalives(__MODULE__, enabled?)
@impl GenServer
def init(opts) do
size = Keyword.get(opts, :size, @default_size)
record_keepalives = Keyword.get(opts, :record_keepalives, true)
{:ok, %{buffer: CircularBuffer.new(size), record_keepalives: record_keepalives}}
end
@impl GenServer
def handle_cast({:log, <<0x23, 0x03, _ack_flag>>, _opts}, %{record_keepalives: false} = state) do
{:noreply, state}
end
def handle_cast({:log, binary, opts}, state) do
record = Record.new(binary, opts)
{:noreply, %{state | buffer: CircularBuffer.insert(state.buffer, record)}}
end
@impl GenServer
def handle_call(:clear, _from, %{buffer: %CircularBuffer{max_size: size}} = state) do
{:reply, :ok, %{state | buffer: CircularBuffer.new(size)}}
end
def handle_call(:list, _from, %{buffer: buffer} = state) do
{:reply, CircularBuffer.to_list(buffer), state}
end
def handle_call({:record_keepalives, enabled?}, _from, state) do
{:reply, :ok, %{state | record_keepalives: enabled?}}
end
def handle_call({:resize, size}, _from, %{buffer: buffer} = state) do
new_buffer =
buffer
|> CircularBuffer.to_list()
|> Enum.reduce(CircularBuffer.new(size), fn record, buffer ->
CircularBuffer.insert(buffer, record)
end)
{:reply, :ok, %{state | buffer: new_buffer}}
end
end