kagux / ex_debug_toolbar

A debug web toolbar for Phoenix projects to display all sorts of information about request

Home Page:https://hex.pm/packages/ex_debug_toolbar

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Proposed new API for collecting toolbar data

juanperi opened this issue · comments

The idea is to back the collection of all the data in independent data structures.
The following is an example of usage in a plug, that keeps track of the original conn, the final conn (useless, i know :) ) and the time that the request takes

defmodule ExDebugToolbar.Plug.Request do
  import Plug.Conn
  alias ExDebugToolbar.Collector.{Timed, Static}
  alias ExDebugToolbar.Toolbar

  def init(default), do: default
  def call(%Plug.Conn{} = conn, _default) do
    # first do something that puts the request id in the current process (and generates it if not there)

    # Static "collector". This one expects to be called only once per name, and stores the "data" in the registry
    Toolbar.attach(%Static{name: :original_conn, data: conn})
    # Timed "collector". The first time it is called with a given name, it saves the current time. The second time
    # it is called, it stores the finish time, and calculates the diff. That is done in the finish/1 callback
    Toolbar.attach(%Timed{name: :request_time})
    Plug.Conn.register_before_send(conn, &finish/1)
  end

  defp finish(conn) do
    Toolbar.attach(%Static{name: :final_conn, data: conn})
    Toolbar.attach(%Timed{name: :request_time})
    conn
  end
end

Definition of collector. The protocol and the behavior allow us to have completely independen handling from one collector to another

defprotocol ExDebugToolbar.Collector do
  @doc "global name of the collector. There is only one of this per request_id"
  def name(data)

  @doc "returns a module that implements the ExDebugToolbar.Updater behavior"
  def updater(data)
end
defmodule ExDebugToolbar.Updater do
  @callback update(previous_state, data) :: new_state
end

this is how the entrypoint looks like

defmodule ExDebugToolbar.Toolbar do
  def attach(collector), do: Process.get(:request_id) |> attach(collector)
  def attach(request_id, collector) do
    current_request = case lookup(request_id) do
      nil -> create(request_id)
      request -> request
    end
    updater = ExDebugToolbar.Collector.updater(collector)
    name = ExDebugToolbar.Collector.name(collector)

    current_data = Map.fetch(current_request, name, nil)
    next_data = updater.update(current_data, collector)

    Registry.update(request_id, Map.put(current_request, name, next_data))
  end
end

and 3 examples of implementation of low level collectors

defmodule ExDebugToolbar.Collector.Timed do
  defstruct [:name, :started_at, :ended_at, :duration]
  @behaviour ExDebugToolbar.Updater

  def update(nil, %Timed{name: name} = data) when not is_nil(name) do
    %{timed | started_at: DateTime.utc_now()}
  end
  def update(%Timed{} = previous_state, _data) do
    ended_at = DateTime.utc_now()
    duration = DateTime.to_unix(ended_at, :microsecond) - DateTime.to_unix(previous_state.started_at, :microsecond)
    %{previous_state | ended_at: ended_at, duration: duration}
  end
end
defimpl ExDebugToolbar.Collector, for: ExDebugToolbar.Collector.Timed do
  def name(collector) do
    collector.name
  end
  def updater(_) do
    ExDebugToolbar.Collector.Timed
  end
end

defmodule ExDebugToolbar.Collector.Log do
  defstruct [:name, :entries, :entry]
  @behaviour ExDebugToolbar.Updater

  def update(nil, %Log{name: name} = data) when not is_nil(name) do
    data
  end
  def update(%Log{entries: entries} = previous_state, %Log{entry: entry} = data) do
    previous_state | entries: (entries ++ [entry])
  end
end
defimpl ExDebugToolbar.Collector, for: ExDebugToolbar.Collector.Log do
  def name(log) do
    log.name
  end
  def updater(_) do
    ExDebugToolbar.Collector.Log
  end
end

defmodule ExDebugToolbar.Collector.Static do
  defstruct [:name, :data]
  @behaviour ExDebugToolbar.Updater

  def update(nil, %Static{name: name} = data) when not is_nil(name) do
    data
  end
end
defimpl ExDebugToolbar.Collector, for: ExDebugToolbar.Collector.Static do
  def name(data) do
    data.name
  end
  def updater(_) do
    ExDebugToolbar.Collector.Static
  end
end

I'm not sure if this compiles, but i think it conveys the idea I had about how to get the data into the registry.
Naming can be improved of course

now that I think of it, name should probably be moved to the outside of the collector. eg:
def attach(request_id, name, collector) do

implemented in #5