Current section

Files

Jump to
ring_logger lib ring_logger server.ex
Raw

lib/ring_logger/server.ex

defmodule RingLogger.Server do
use GenServer
@moduledoc false
@default_max_size 1024
alias RingLogger.Client
defmodule State do
@moduledoc false
defstruct clients: [],
cb: nil,
index: 0
end
@spec start_link([RingLogger.server_option()]) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@spec stop() :: :ok
def stop() do
GenServer.stop(__MODULE__)
end
@doc """
Fetch the current configuration for the server and any attached clients.
"""
@spec config() :: [RingLogger.server_option()]
def config() do
GenServer.call(__MODULE__, :config)
end
@spec configure([RingLogger.server_option()]) :: :ok
def configure(opts) do
GenServer.call(__MODULE__, {:configure, opts})
end
@spec attach_client(pid()) :: :ok
def attach_client(client_pid) do
GenServer.call(__MODULE__, {:attach, client_pid})
end
@spec detach_client(pid()) :: :ok
def detach_client(client_pid) do
GenServer.call(__MODULE__, {:detach, client_pid})
end
@spec get(non_neg_integer(), non_neg_integer()) :: [RingLogger.entry()]
def get(start_index, n) do
GenServer.call(__MODULE__, {:get, start_index, n})
end
@spec log(
Logger.level(),
{Logger, Logger.message(), Logger.Formatter.time(), Logger.metadata()}
) :: :ok
def log(level, message) do
GenServer.cast(__MODULE__, {:log, level, message})
end
@spec tail(non_neg_integer()) :: [RingLogger.entry()]
def tail(n) do
GenServer.call(__MODULE__, {:tail, n})
end
@spec clear() :: :ok
def clear() do
GenServer.call(__MODULE__, :clear)
end
@impl GenServer
def init(opts) do
max_size = Keyword.get(opts, :max_size, @default_max_size)
{:ok, %State{cb: CircularBuffer.new(max_size)}}
end
@impl GenServer
def handle_call(:clear, _from, state) do
max_size = state.cb.max_size
{:reply, :ok, %{state | cb: CircularBuffer.new(max_size)}}
end
def handle_call(:config, _from, state) do
config = %{max_size: state.cb.max_size}
{:reply, config, state}
end
def handle_call({:configure, opts}, _from, state) do
case Keyword.get(opts, :max_size) do
nil ->
{:reply, :ok, state}
max_size ->
{:reply, :ok, %State{state | cb: CircularBuffer.new(max_size)}}
end
end
def handle_call({:attach, client_pid}, _from, state) do
{:reply, :ok, attach_client(client_pid, state)}
end
def handle_call({:detach, pid}, _from, state) do
{:reply, :ok, detach_client(pid, state)}
end
def handle_call({:get, start_index, 0}, _from, state) do
first_index = state.index - Enum.count(state.cb)
adjusted_start_index = max(start_index - first_index, 0)
items = Enum.drop(state.cb, adjusted_start_index)
{:reply, items, state}
end
def handle_call({:get, start_index, n}, _from, state) do
first_index = state.index - Enum.count(state.cb)
last_index = state.index
{adjusted_start_index, adjusted_n} =
{start_index, n}
|> adjust_left(first_index)
|> adjust_right(last_index)
items = Enum.slice(state.cb, adjusted_start_index, adjusted_n)
{:reply, items, state}
end
def handle_call({:tail, n}, _from, state) do
{:reply, Enum.take(state.cb, -n), state}
end
@impl GenServer
def handle_cast({:log, level, message}, state) do
{:noreply, push(level, message, state)}
end
@impl GenServer
def handle_info({:DOWN, _ref, _, pid, _reason}, state) do
{:noreply, detach_client(pid, state)}
end
@impl GenServer
def terminate(_reason, state) do
Enum.each(state.clients, fn {client_pid, _ref} -> Client.stop(client_pid) end)
:ok
end
defp adjust_left({offset, n}, i) when i > offset do
{i, max(0, n - (i - offset))}
end
defp adjust_left(loc, _i), do: loc
defp adjust_right({offset, n}, i) when i < offset + n do
{offset, i - offset}
end
defp adjust_right(loc, _i), do: loc
defp attach_client(client_pid, state) do
if !client_info(client_pid, state) do
ref = Process.monitor(client_pid)
%{state | clients: [{client_pid, ref} | state.clients]}
else
state
end
end
defp detach_client(client_pid, state) do
case client_info(client_pid, state) do
{_client_pid, ref} ->
Process.demonitor(ref)
remaining_clients = List.keydelete(state.clients, client_pid, 0)
%{state | clients: remaining_clients}
nil ->
state
end
end
defp client_info(client_pid, state) do
List.keyfind(state.clients, client_pid, 0)
end
defp push(level, {mod, msg, ts, md}, state) do
index = state.index
log_entry = {level, {mod, msg, ts, Keyword.put(md, :index, index)}}
Enum.each(state.clients, &send_log(&1, log_entry))
new_cb = CircularBuffer.insert(state.cb, log_entry)
%{state | cb: new_cb, index: index + 1}
end
defp send_log({client_pid, _ref}, log_entry) do
send(client_pid, {:log, log_entry})
end
end