defmodule Caddy.Logger.Store do
@moduledoc """
GenServer that stores Caddy server log history.
Maintains a rolling buffer of up to 50,000 log lines from the Caddy
server. Provides access to recent logs via the `tail/1` function.
## Telemetry Events
This module emits the following telemetry events:
* `[:caddy, :log, :stored]` - When a log line is stored.
Measurements: `%{store_size: integer(), duration: integer()}`
Metadata: `%{message: binary(), trimmed: boolean()}`
* `[:caddy, :log, :retrieved]` - When logs are retrieved via tail.
Measurements: `%{lines: integer(), duration: integer()}`
Metadata: `%{requested: integer(), available: integer()}`
"""
require Logger
@keep_lines 50_000
use GenServer
def write(log) do
GenServer.cast(__MODULE__, {:write, log})
end
def tail(num \\ 100) do
GenServer.call(__MODULE__, {:tail, num})
end
@spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()}
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init(args) do
{:ok, args}
end
def handle_cast({:write, log}, state) do
start_time = System.monotonic_time()
new_state = [log | state] |> Enum.take(@keep_lines)
trimmed = length(state) + 1 > @keep_lines
duration = System.monotonic_time() - start_time
# Emit stored event
Caddy.Telemetry.emit_log_event(
:stored,
%{
store_size: length(new_state),
duration: duration
},
%{message: log, trimmed: trimmed}
)
{:noreply, new_state}
end
def handle_call({:tail, n}, _from, state) do
start_time = System.monotonic_time()
logs = state |> Enum.take(n) |> Enum.reverse()
duration = System.monotonic_time() - start_time
# Emit retrieved event
Caddy.Telemetry.emit_log_event(
:retrieved,
%{
lines: length(logs),
duration: duration
},
%{requested: n, available: length(state)}
)
{:reply, logs, state}
end
end