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, RecordQueue}

  @type src() :: String.t()
  @type dest() :: String.t()

  @type log_opt() :: {:src, src()} | {:dest, dest()}

  @type format() :: :text | :term

  @doc """
  Start the trace server
  """
  @spec start_link(keyword()) :: GenServer.on_start()
  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  @doc """
  Serializes 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_parameters`. This is the default format.

  * `:term` - The trace is serialized as a list of `Record.t()` structs in
    Erlang external term format. This is useful if you want to load the trace
    on another machine.
  """
  @spec format([Record.t()], format()) :: binary()
  def format(records, format \\ :text)
  def format(records, :text), do: records_to_contents(records)
  def format(records, :term), do: :erlang.term_to_binary(records)

  @doc """
  Dump the trace records into a file. See `format/2` for the available formats.
  """
  @spec dump(binary(), format()) :: :ok | {:error, atom()}
  def dump(file, format \\ :text) do
    file_contents = format(list(), format)
    File.write(file, file_contents)
  end

  @doc """
  Log the trace information
  """
  @spec log(binary(), [log_opt()]) :: :ok
  def log(binary, opts \\ []) do
    GenServer.cast(__MODULE__, {:log, binary, opts})
  end

  @doc """
  Force clear the records from the trace
  """
  @spec clear() :: :ok
  def clear() do
    GenServer.call(__MODULE__, :clear)
  end

  @doc """
  List all the records currently being traced
  """
  @spec list() :: [Record.t()]
  def list() do
    GenServer.call(__MODULE__, :list)
  end

  @impl GenServer
  def init(_args) do
    {:ok, RecordQueue.new()}
  end

  @impl GenServer
  def handle_cast({:log, binary, opts}, records) do
    record = Record.new(binary, opts)

    {:noreply, RecordQueue.add_record(records, record)}
  end

  @impl GenServer
  def handle_call(:clear, _from, _records) do
    {:reply, :ok, RecordQueue.new()}
  end

  def handle_call(:list, _from, records) do
    {:reply, RecordQueue.to_list(records), records}
  end

  defp records_to_contents(records) do
    Enum.reduce(records, "", fn record, str ->
      str <> Record.to_string(record) <> "\n"
    end)
  end
end