defmodule ErrorLogger do require Logger @type message :: binary | [message] @type kind :: :error | :exit @type error :: any @doc """ Formats and logs the given error using `Exception.format/3` and `Logger.error/1`. Depending on from where this macro is called, it will also include the stacktrace that was recorded with the error in the log message. `error` can be anything, special formatting will be done for certain Erlang error atoms or tuples, as well as Elixir exceptions. `log_level` (default `:error`) can be overwritten to log in a different level `metadata` optional metadata passed into the Logger """ defmacro log_error(error, log_level \\ :error, metadata \\ []) do quote bind_quoted: [error: error, log_level: log_level, metadata: metadata] do log_error_raw(nil, :error, error, log_level, metadata) end end @doc """ Like `log_error/3`, but also puts the given`message` before the formatted error. """ defmacro log_message_and_error(message, error, log_level \\ :error, metadata \\ []) do quote bind_quoted: [message: message, error: error, log_level: log_level, metadata: metadata] do log_error_raw(message, :error, error, log_level, metadata) end end @doc """ Like `log_error/3`, but is used to log caught exits. When the pid is given, it will pass `{:EXIT, pid}` instead of `:exit` to `Exception.format/3`. """ defmacro log_exit(error, pid \\ nil, log_level \\ :error, metadata \\ []) defmacro log_exit(error, nil, log_level, metadata) do quote bind_quoted: [error: error, log_level: log_level, metadata: metadata] do log_error_raw(nil, :exit, error, log_level, metadata) end end defmacro log_exit(error, pid, log_level, metadata) do quote bind_quoted: [error: error, log_level: log_level, metadata: metadata, pid: pid] do log_error_raw(nil, {:EXIT, pid}, error, log_level, metadata) end end @doc """ Formats and logs the given `error` of the given `kind` with an optional preceding `message`. """ defmacro log_error_raw(message, kind, error, log_level \\ :error, metadata \\ []) do quote do unquote(error) |> unquote(__MODULE__).format_error(unquote(kind)) |> unquote(__MODULE__).log_formatted(unquote(message), unquote(log_level), unquote(metadata)) end end @doc """ Formats the given error using `Exception.format/3`. Adds the current stack trace, if available. """ defmacro format_error(error, kind) do if in_catch?(__CALLER__) do quote do Exception.format(unquote(kind), unquote(error), __STACKTRACE__) end else quote do Exception.format(unquote(kind), unquote(error)) end end end @doc """ Logs a formatted error with an optional preceding message. """ @spec log_formatted(binary, message, level :: Logger.level, metadata :: Logger.metadata) :: no_return def log_formatted(formatted, message, level \\ :error, metadata \\ []) do log_message = [message, formatted] |> Enum.reject(&is_nil/1) |> Enum.join("\n") Logger.log(level, log_message, metadata) end @doc """ Returns if the environment is within a catch or rescue clause. Use it like this in a macro: defmacro maybe_get_stacktrace() do if in_catch?(__CALLER__) do quote do __STACKTRACE__ end else quote do [] end end end """ @spec in_catch?(Macro.Env.t) :: boolean def in_catch?(env) do env |> Map.get(:contextual_vars, []) |> Enum.member?(:__STACKTRACE__) end end