~ihabunek/triglav

bf267dd850483e468b338c54b4156a0863be89d3 — Ivan Habunek 8 months ago d0e7c46
Add some helper fns
1 files changed, 81 insertions(+), 0 deletions(-)

A lib/triglav/tooling.ex
A lib/triglav/tooling.ex => lib/triglav/tooling.ex +81 -0
@@ 0,0 1,81 @@
defmodule Triglav.Tooling do
  require Logger

  @doc """
  Returns mix environment in which this file was compiled.

  Unlike `Mix.env` this function can be safely used at runtime.

  Compared to the `@mix_env Mix.env` pattern, this fuction will not lead to dialyzer warnings.
  Consider the following code:

      @mix_env Mix.env()

      def foo do
        if @mix_env == :test, do: ...
      end

  Since `@mix_env` is resolved at compile time, the generated code in e.g. `:dev` mix env is going
  to be `if :dev == :test, do: ...` which is always false and leads to dialyzer warnings.

  In contrast, `if Tooling.mix_env() == :test, do: ...` won't cause such warnings.
  """
  @spec mix_env :: :dev | :test | :prod
  def mix_env do
    # We're tricking dialyzer by doing a persistent term lookup under an unknown key. As a result,
    # dialyzer can't deduce the exact value this function returns.
    :persistent_term.get({__MODULE__, :non_existent_key}, unquote(Mix.env()))
  end

  @doc """
  Formats the error for logging with respect to logger truncation.

  This function reimplements `Exception.format_error/3` in a way that maximizes useful information. The issue with
  format_error is that if the error message is large (e.g. because large structure is inspected), the stacktrace won't
  be visible in the logged error (because of logger truncation).

  This function truncates the formatted error reason, to make sure that stacktrace will be visible in the log output, as
  configured in the logger via the `:truncate` option.
  """
  @spec format_error(String.t(), any, Exception.stacktrace()) :: iodata()
  def format_error(prefix \\ "", error, stack_entries) do
    # we'll also remove the leading `** ` because we're prepending the prefix to
    # the message
    banner =
      Exception.format_banner(:error, error, stack_entries)
      |> String.replace(~r/^\*\* /, "")

    stacktrace =
      if stack_entries == [],
        do: "",
        else: "\n#{Exception.format_stacktrace(stack_entries)}"

    max_length = Application.fetch_env!(:logger, :truncate)

    if String.length(prefix) + String.length(banner) + String.length(stacktrace) <= max_length do
      [prefix, banner, stacktrace]
    else
      # We're truncating the banner (formatted error reason) to make sure that
      # stacktrace is present in the output. However, we're not going below
      # some minimum, to improve the message usefulness. Therefore, if the
      # stacktrace is also very large, the final message will exceed the
      # logger's truncate limit. But in this case, we'll still be able to see
      # some part of the message and the stacktrace. Note that this situation
      # is in practice very unlikely.
      suffix = " ..."

      content_length = String.length(prefix) + String.length(suffix) + String.length(stacktrace)
      banner_length = max_length - content_length

      banner_length = max(banner_length, _min_banner_length = 100)
      banner = "#{String.slice(banner, 0, banner_length)} ..."

      [prefix, banner, stacktrace]
    end
  end

  def log_error(message, error, stack_entries) do
    Logger.error(format_error(message, error, stack_entries))
    Sentry.capture_exception(error, stacktrace: stack_entries, extra: %{message: message})
  end
end