Current section
Files
Jump to
Current section
Files
lib/ex_dbug.ex
defmodule ExDbug do
@external_resource "README.md"
@moduledoc File.read!("README.md")
|> String.split("## Contributing")
|> List.first()
|> String.trim()
require Logger
@type debug_opts :: [
enabled: boolean(),
context: atom() | String.t(),
max_depth: non_neg_integer(),
include_timing: boolean(),
include_stack: boolean(),
max_length: non_neg_integer(),
truncate_threshold: non_neg_integer()
]
defmacro __using__(opts \\ []) do
enabled = get_debug_enabled(opts)
if enabled do
quote do
import ExDbug
require Logger
require ExDbug
@debug_opts ExDbug.merge_options(unquote(opts))
@context Keyword.get(unquote(opts), :context, __MODULE__) |> to_string()
@before_compile ExDbug
end
else
quote do
require Logger
defmacro dbug(_, _ \\ [], _ \\ []), do: nil
defmacro error(_, _ \\ [], _ \\ []), do: nil
def __debug_context__, do: nil
end
end
end
defmacro __before_compile__(_env) do
quote do
def __debug_context__, do: @context
end
end
defmacro dbug(message, metadata \\ [], opts \\ []) do
quote bind_quoted: [message: message, metadata: metadata, opts: opts] do
context = __debug_context__()
ExDbug.log(:debug, message, metadata, Map.new(@debug_opts))
end
end
defmacro error(message, metadata \\ [], opts \\ []) do
quote bind_quoted: [message: message, metadata: metadata, opts: opts] do
context = __debug_context__()
ExDbug.log(:error, message, metadata, Map.new(@debug_opts))
end
end
defmacro track(value, name) do
quote do
result = unquote(value)
dbug("Value tracked: #{unquote(name)} = #{inspect(result)}")
result
end
end
@doc false
def get_debug_enabled(opts) do
env_enabled = Application.get_env(:ex_dbug, :enabled, true)
Keyword.get(opts, :enabled, env_enabled)
end
@doc false
def merge_options(opts) do
defaults = [
max_depth: 3,
include_timing: true,
include_stack: true,
max_length: 500,
truncate_threshold: 100,
levels: [:debug, :error]
]
app_config = Application.get_env(:ex_dbug, :config, [])
Keyword.merge(defaults, app_config) |> Keyword.merge(opts)
end
@doc false
def format_output(message, context) do
format_output(message, context, [])
end
@doc false
def format_output(message, context, metadata, opts \\ %{}) do
formatted_metadata = format_metadata(metadata, opts)
base = "[#{context}] #{message}"
if formatted_metadata != "", do: "#{base} #{formatted_metadata}", else: base
end
@doc false
def log(level, message, metadata, context)
when level in [:debug, :error] and (is_binary(context) or is_atom(context)) do
context_str = to_string(context)
if should_log?(level, metadata, context_str) do
formatted = format_output(message, context_str, metadata)
case level do
:debug -> Logger.debug(formatted)
:error -> Logger.error(formatted)
end
end
end
@doc false
def log(level, message, metadata, opts) when level in [:debug, :error] and is_map(opts) do
context = Map.get(opts, :context) || "unknown"
log(level, message, metadata, context)
end
@doc false
defp should_log?(level, metadata, context) do
debug_levels = Keyword.get(metadata, :levels, [:debug, :error])
level_allowed = level in debug_levels
pattern_match = namespace_enabled?(context)
level_allowed and pattern_match
end
@doc false
defp namespace_enabled?(context) do
patterns = parse_debug_env()
matches_namespace?(context, patterns)
end
@doc false
defp parse_debug_env do
debug_val = System.get_env("DEBUG", "")
parse_patterns(debug_val)
end
@doc false
defp parse_patterns(string) when is_binary(string) do
raw = String.split(string, [",", " "], trim: true)
{includes, excludes} =
Enum.reduce(raw, {[], []}, fn pattern, {inc, exc} ->
pattern = String.trim(pattern)
cond do
pattern == "" ->
{inc, exc}
String.starts_with?(pattern, "-") ->
{inc, [String.trim_leading(pattern, "-") | exc]}
true ->
{[pattern | inc], exc}
end
end)
{Enum.reverse(includes), Enum.reverse(excludes)}
end
@doc false
defp matches_namespace?(namespace, {includes, excludes}) do
cond do
includes == [] and excludes == [] ->
true
true ->
included = includes == [] or Enum.any?(includes, &wildcard_match?(namespace, &1))
excluded = Enum.any?(excludes, &wildcard_match?(namespace, &1))
included and not excluded
end
end
@doc false
defp wildcard_match?(string, pattern) do
regex_pattern =
pattern
|> Regex.escape()
|> String.replace("\\*", ".*")
Regex.match?(Regex.compile!("^" <> regex_pattern <> "$"), string)
end
# Private helper functions for metadata formatting
defp format_metadata(metadata, opts) when is_list(metadata) do
max_length = Map.get(opts, :max_length, 500)
truncate_threshold = Map.get(opts, :truncate_threshold, 100)
case metadata do
[] ->
""
_ ->
Enum.map_join(metadata, ", ", fn {key, value} ->
formatted_value = format_value(value, max_length, truncate_threshold)
"#{key}: #{formatted_value}"
end)
end
end
defp format_metadata(_, _), do: ""
defp format_value(value, max_length, truncate_threshold) do
formatted = inspect(value, limit: :infinity, pretty: false)
if String.length(formatted) > truncate_threshold do
truncated = String.slice(formatted, 0, max_length)
"#{truncated}... (truncated)"
else
formatted
end
end
end