@@ 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