Skip to content

Instantly share code, notes, and snippets.

@danielres
Created November 8, 2025 15:53
Show Gist options
  • Select an option

  • Save danielres/98dac5fcf042ea9984ecf6180e86c352 to your computer and use it in GitHub Desktop.

Select an option

Save danielres/98dac5fcf042ea9984ecf6180e86c352 to your computer and use it in GitHub Desktop.
Dev-only pretty formatter for elixir Phoenix errors
# config/dev.exs
import Config
config :phoenix, :stacktrace_depth, 60
config :logger, level: :info
config :logger, :console,
format: {PrettyConsoleFormatter, :format},
# keep only the most useful metadata; add :request_id if you use Plug.RequestId
metadata: [:request_id, :mfa, :file, :line, :pid],
colors: [enabled: true],
truncate: :infinity
# lib/pretty_console_formatter.ex
defmodule PrettyConsoleFormatter do
@moduledoc false
@levels %{debug: "DEBUG", info: " INFO", warn: " WARN", error: "ERROR"}
# Console backend callback: {mod, fun} with arity 4
def format(level, message, _ts, md) do
# normalize iodata -> binary
msg = IO.iodata_to_binary(message)
lines =
msg
|> String.replace_trailing("\n", "")
|> String.split("\n", trim: true)
{head, tail} =
case lines do
[h | t] -> {h, t}
[] -> {"", []}
end
# Optional request context
req = case Keyword.get(md, :request_id) do
nil -> ""
rid -> " · req=#{rid}"
end
# Color helpers (works in IEx and most terminals)
color = fn s, code -> ["\e[", code, "m", s, "\e[0m"] end
dim = fn s -> color.(s, "90") end
# Level badge + first line
first =
[
"\n",
badge(level, color),
" ",
color.(String.trim(head), level_color(level)),
req,
"\n"
]
# Format remaining lines (stacktrace etc.)
formatted_tail =
tail
|> Enum.map(&format_tail_line/1)
|> Enum.reject(&(&1 == :drop))
|> Enum.map(fn
{:dim, l} -> [" ", dim.("│ " <> l), "\n"]
{:keep, l} -> [" ", "│ ", l, "\n"]
end)
# Metadata (opt-in, compact)
meta =
md
|> Keyword.take([:mfa, :file, :line, :pid])
|> case do
[] -> []
kw ->
[" ", dim.("└ meta: " <> inspect(kw)), "\n"]
end
[first | [formatted_tail, meta]]
end
# Collapse noisy Phoenix/Plug/Cowboy frames; keep app frames
@noise ~r/^\s*\(?(phoenix|plug|cowboy|ranch|telemetry|ecto|db_connection)\)?/i
defp format_tail_line(line) do
cond do
line == "" ->
:drop
String.match?(line, @noise) ->
{:dim, String.trim_leading(line)}
true ->
{:keep, emphasize_app_frame(line)}
end
end
# Make app frames pop a bit (indent paths, bold function)
defp emphasize_app_frame(line) do
case Regex.run(~r/(.*?)(\([^)]+\))?$/, line) do
[_, pre, fun] when is_binary(fun) ->
pre <> bold(fun)
_ ->
line
end
end
defp bold(s), do: "\e[1m" <> s <> "\e[0m"
defp badge(level, color) do
tag = Map.get(@levels, level, String.upcase(to_string(level)))
case level do
:error -> color.("[#{tag}]", "41;97") # white on red
:warn -> color.("[#{tag}]", "43;30") # black on yellow
:info -> color.("[#{tag}]", "44;97") # white on blue
:debug -> color.("[#{tag}]", "45;97") # white on magenta
_ -> "[#{tag}]"
end
end
defp level_color(:error), do: "31" # red
defp level_color(:warn), do: "33" # yellow
defp level_color(:info), do: "36" # cyan
defp level_color(:debug), do: "35" # magenta
defp level_color(_), do: "37" # white
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment