Created
November 8, 2025 15:53
-
-
Save danielres/98dac5fcf042ea9984ecf6180e86c352 to your computer and use it in GitHub Desktop.
Dev-only pretty formatter for elixir Phoenix errors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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