Current section

Files

Jump to
mob_dev lib mob_dev server elixir_logger.ex
Raw

lib/mob_dev/server/elixir_logger.ex

defmodule MobDev.Server.ElixirLogger do
@moduledoc """
OTP logger handler that captures Elixir `Logger` output and forwards it
to the Mob dev server dashboard.
Attached by `mix mob.server` after the supervision tree starts, so
`ElixirLogBuffer` and PubSub are guaranteed to be running.
Only captures events with `domain: [:elixir]` — the domain Elixir's Logger
uses for all `Logger.info/debug/warning/error` calls. Raw `:logger` calls
and OTP system messages are excluded.
"""
@handler_id :mob_dev_elixir_logger
@topic "elixir_logs"
@doc "Attach the handler to OTP's logger. Call after the server supervisor starts."
@spec attach() :: :ok | {:error, term()}
def attach do
:logger.add_handler(@handler_id, __MODULE__, %{})
end
@doc "Detach the handler."
@spec detach() :: :ok | {:error, term()}
def detach do
:logger.remove_handler(@handler_id)
end
# ── OTP logger callbacks ──────────────────────────────────────────────────
@spec adding_handler(map()) :: {:ok, map()}
def adding_handler(config), do: {:ok, config}
@spec removing_handler(map()) :: :ok
def removing_handler(_config), do: :ok
@spec log(map(), map()) :: :ok
def log(%{level: level, msg: msg, meta: meta} = _event, _config) do
# Only capture Elixir Logger events (domain: [:elixir])
if elixir_domain?(meta) do
line = %{
id: System.unique_integer([:positive, :monotonic]),
level: level_char(level),
message: format_msg(msg),
ts: format_time(meta[:time]),
module: meta[:module]
}
# Guard: if the buffer GenServer isn't up, skip silently
if Process.whereis(MobDev.Server.ElixirLogBuffer) do
MobDev.Server.ElixirLogBuffer.push(line)
end
if Process.whereis(MobDev.PubSub) do
Phoenix.PubSub.broadcast(MobDev.PubSub, @topic, {:elixir_log_line, line})
end
end
:ok
end
# ── Helpers ───────────────────────────────────────────────────────────────
defp elixir_domain?(%{domain: [:elixir | _]}), do: true
defp elixir_domain?(_), do: false
defp level_char(:emergency), do: "E"
defp level_char(:alert), do: "E"
defp level_char(:critical), do: "E"
defp level_char(:error), do: "E"
defp level_char(:warning), do: "W"
defp level_char(:notice), do: "I"
defp level_char(:info), do: "I"
defp level_char(:debug), do: "D"
defp level_char(_), do: "D"
defp format_msg({:string, text}), do: IO.iodata_to_binary(text)
defp format_msg({:report, map}), do: inspect(map, pretty: false, limit: 50)
defp format_msg({:format, fmt, args}) do
:io_lib.format(fmt, args) |> IO.iodata_to_binary()
rescue
_ -> inspect({fmt, args})
end
defp format_msg(other), do: inspect(other)
defp format_time(nil), do: ""
defp format_time(microseconds) do
ms = div(microseconds, 1_000)
dt = DateTime.from_unix!(ms, :millisecond)
frac = String.pad_leading("#{rem(ms, 1000)}", 3, "0")
Calendar.strftime(dt, "%H:%M:%S.") <> frac
end
end