defmodule Pi.LogCapture do
@moduledoc "Bounded Logger capture for embedded pi-elixir sessions."
use GenServer
@levels Map.new(~w[emergency alert critical error warning notice info debug]a, &{"#{&1}", &1})
def start_link(opts \\ []), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def install do
case Process.whereis(__MODULE__) do
nil -> start_link([])
_pid -> :ok
end
case :logger.add_handler(__MODULE__, __MODULE__, %{
formatter: Logger.default_formatter(colors: [enabled: false])
}) do
:ok -> :ok
{:error, {:already_exists, _pid}} -> :ok
{:error, _reason} -> :ok
end
end
def get_logs(n, opts \\ []) do
grep = Keyword.get(opts, :grep)
regex = grep && Regex.compile!(grep, "iu")
level = Keyword.get(opts, :level)
level_atom = normalize_level(level)
GenServer.call(__MODULE__, {:get_logs, n, regex, level_atom})
end
def clear_logs, do: GenServer.call(__MODULE__, :clear_logs)
def log(%{meta: meta, level: level} = event, config) do
if meta[:pi_bridge] do
:ok
else
%{formatter: {formatter_mod, formatter_config}} = config
chardata = formatter_mod.format(event, formatter_config)
GenServer.cast(__MODULE__, {:log, level, IO.chardata_to_string(chardata)})
end
end
@impl true
def init(opts) do
{:ok, %{logs: :queue.new(), size: 0, max: Keyword.get(opts, :max, 1024)}}
end
@impl true
def handle_cast({:log, level, message}, state) do
{logs, size} =
if state.size >= state.max do
{_, q} = :queue.out(state.logs)
{:queue.in({level, message}, q), state.size}
else
{:queue.in({level, message}, state.logs), state.size + 1}
end
{:noreply, %{state | logs: logs, size: size}}
end
@impl true
def handle_call({:get_logs, n, regex, level_filter}, _from, state) do
logs = :queue.to_list(state.logs)
logs =
if level_filter do
Enum.filter(logs, fn {level, _} -> level == level_filter end)
else
logs
end
logs =
if regex do
Enum.filter(logs, fn {_, message} -> Regex.match?(regex, message) end)
else
logs
end
messages = Enum.map(logs, &elem(&1, 1))
{:reply, Enum.take(messages, -n), state}
end
def handle_call(:clear_logs, _from, state) do
{:reply, :ok, %{state | logs: :queue.new(), size: 0}}
end
defp normalize_level(nil), do: nil
defp normalize_level(level) when is_atom(level), do: level
defp normalize_level(level) when is_binary(level), do: Map.fetch!(@levels, level)
end