Packages
mob_dev
0.5.5
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
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