Current section

Files

Jump to
disco_log lib disco_log logger_handler.ex
Raw

lib/disco_log/logger_handler.ex

defmodule DiscoLog.LoggerHandler do
@moduledoc """
A logger handler [`:logger` handler](https://www.erlang.org/doc/apps/kernel/logger_chapter.html#handlers)
Original source: https://github.com/getsentry/sentry-elixir/blob/69ac8d0e3f33ff36ab1092bbd346fdb99cf9d061/lib/sentry/logger_handler.ex
"""
alias __MODULE__
alias DiscoLog.Client
alias DiscoLog.Config
alias DiscoLog.Context
alias DiscoLog.Error
defstruct excluded_domains: [:cowboy, :bandit],
metadata: []
## Logger handler callbacks
@spec adding_handler(:logger.handler_config()) :: {:ok, :logger.handler_config()}
def adding_handler(config) do
config = Map.put(config, :config, configure(%LoggerHandler{}, Config.logger_config()))
{:ok, config}
end
@spec changing_config(:update, :logger.handler_config(), :logger.handler_config()) ::
{:ok, :logger.handler_config()}
def changing_config(:update, old_config, _new_config) do
new_config = Map.put(old_config, :config, configure(%LoggerHandler{}, Config.logger_config()))
{:ok, new_config}
end
defp configure(%LoggerHandler{} = existing_config, config) do
metadata = Keyword.get(config, :metadata, [])
%{
existing_config
| metadata: metadata
}
end
def removing_handler(_) do
:ok
end
def log(%{level: log_level, meta: log_meta} = log_event, %{
config: %LoggerHandler{} = config
}) do
cond do
excluded_level?(log_level) ->
:ok
excluded_domain?(Map.get(log_meta, :domain, []), config.excluded_domains) ->
:ok
log_level == :info and Map.get(log_meta, :application) == :phoenix ->
:ok
log_level == :info ->
log_info(log_event, config)
true ->
log_error(log_event, config)
end
end
defp excluded_level?(log_level) do
Logger.compare_levels(log_level, :error) == :lt and log_level != :info
end
defp excluded_domain?(logged_domains, excluded_domains) do
Enum.any?(logged_domains, &(&1 in excluded_domains))
end
# "string" logged
defp log_info(
%{msg: {:string, unicode_chardata}, meta: meta},
%LoggerHandler{} = config
) do
metadata = take_metadata(meta, config.metadata)
message = IO.iodata_to_binary(unicode_chardata)
Client.log_info(message, metadata)
:ok
end
# "report" here is of type logger:report/0, which is a struct, map or keyword list.
defp log_info(
%{msg: {:report, report}, meta: meta},
%LoggerHandler{} = config
) do
metadata = take_metadata(meta, config.metadata)
log_info_report(report, metadata)
:ok
end
# erlang `:logger` support this format ex `:logger.info("Hello ~s", ["world"])`
# read more on: https://www.erlang.org/doc/apps/kernel/logger_chapter.html#log-message
defp log_info(
%{msg: {format, format_args}, meta: meta},
%LoggerHandler{} = config
) do
metadata = take_metadata(meta, config.metadata)
string_message = format |> :io_lib.format(format_args) |> IO.chardata_to_string()
Client.log_info(string_message, metadata)
:ok
end
defp log_info(_log_event, %LoggerHandler{} = _config) do
:ok
end
# A string was logged. We check for the :crash_reason metadata and try to build a sensible
# report from there, otherwise we use the logged string directly.
defp log_error(
%{msg: {:string, unicode_chardata}, meta: meta} = log_event,
%LoggerHandler{} = config
) do
metadata = take_metadata(log_event.meta, config.metadata)
log_from_crash_reason(meta[:crash_reason], unicode_chardata, metadata)
end
# "report" here is of type logger:report/0, which is a struct, map or keyword list.
defp log_error(
%{msg: {:report, report}, meta: meta},
%LoggerHandler{} = config
) do
metadata = take_metadata(meta, config.metadata)
log_error_report(report, metadata)
:ok
end
# erlang `:logger` support this format ex `:logger.error("Hello ~s", ["world"])`
# read more on: https://www.erlang.org/doc/apps/kernel/logger_chapter.html#log-message
defp log_error(
%{msg: {format, format_args}, meta: meta},
%LoggerHandler{} = config
) do
metadata = take_metadata(meta, config.metadata)
string_message = format |> :io_lib.format(format_args) |> IO.chardata_to_string()
Client.log_error(string_message, metadata)
:ok
end
defp log_error(_log_event, %LoggerHandler{} = _config) do
:ok
end
defp log_from_crash_reason(
{exception, stacktrace},
_chardata_message,
metadata
)
when is_exception(exception) and is_list(stacktrace) do
context = Map.put(Context.get(), :metadata, metadata)
error = Error.new(exception, stacktrace, context)
Client.send_error(error)
:ok
end
defp log_from_crash_reason(
{reason, stacktrace},
chardata_message,
metadata
)
when is_list(stacktrace) do
context =
Map.merge(Context.get(), %{
extra_info_from_message: extra_info_from_message(chardata_message),
metadata: metadata
})
case reason do
{type, {GenServer, :call, [_pid, _call, _timeout]}} = reason
when type in [:noproc, :timeout] ->
reason = Exception.format_exit(reason)
context = Map.put(context, :extra_reason, reason)
error = Error.new({"genserver_call", type}, stacktrace, context)
Client.send_error(error)
_other ->
context =
Map.put(context, :extra_info_from_genserver, try_to_parse_message(chardata_message))
error = Error.new(reason, stacktrace, context)
Client.send_error(error)
end
:ok
end
defp log_from_crash_reason(
_other_reason,
chardata_message,
metadata
) do
Client.log_error(chardata_message, metadata)
:ok
end
defp extra_info_from_message([
[
"GenServer ",
_pid,
" terminating",
_reason,
"\nLast message",
_from,
": ",
last_message
],
"\nState: ",
state | _rest
]) do
%{genserver_state: state, last_message: last_message}
end
# Sometimes there's an extra sneaky [] in there.
defp extra_info_from_message([
[
"GenServer ",
_pid,
" terminating",
_reason,
[],
"\nLast message",
_from,
": ",
last_message
],
"\nState: ",
state | _rest
]) do
%{genserver_state: state, last_message: last_message}
end
defp extra_info_from_message(_message) do
%{}
end
# We do this because messages from Erlang's gen_* behaviours are often full of interesting
# and useful data. For example, GenServer messages contain the PID, the reason, the last
# message, and a treasure trove of stuff. If we cannot parse the message, such is life
# and we just report it as is.
defp try_to_parse_message([
[
"GenServer ",
_inspected_pid,
" terminating",
chardata_reason,
_whatever_this_is = [],
"\nLast message",
[" (from ", _inspected_sender_pid, ")"],
": ",
inspected_last_message
],
"\nState: ",
inspected_state | _
]) do
string_reason = chardata_reason |> :unicode.characters_to_binary() |> String.trim()
%{
last_message: inspected_last_message,
message: "GenServer %{} terminating: #{string_reason}",
state: inspected_state
}
end
defp try_to_parse_message([
[
"GenServer ",
_inspected_pid,
" terminating",
chardata_reason,
"\nLast message",
[" (from ", _inspected_sender_pid, ")"],
": ",
inspected_last_message
],
"\nState: ",
inspected_state | _
]) do
string_reason = chardata_reason |> :unicode.characters_to_binary() |> String.trim()
%{
last_message: inspected_last_message,
message: "GenServer %{} terminating: #{string_reason}",
state: inspected_state
}
end
defp try_to_parse_message([
[
"GenServer ",
_inspected_pid,
" terminating",
chardata_reason,
"\nLast message: ",
inspected_last_message
],
"\nState: ",
inspected_state | _
]) do
string_reason = chardata_reason |> :unicode.characters_to_binary() |> String.trim()
%{
last_message: inspected_last_message,
message: "GenServer %{} terminating: #{string_reason}",
state: inspected_state
}
end
defp try_to_parse_message(_chardata_message) do
%{}
end
# Some metadata like PID are not serializable to JSON a better approach is
# needed
defp take_metadata(metadata, keys) do
Enum.reduce(keys, %{}, fn key, acc ->
case Map.fetch(metadata, key) do
{:ok, val} -> Map.put(acc, key, val)
:error -> acc
end
end)
end
defp log_info_report(report, metadata) when is_struct(report) do
report
|> Map.from_struct()
|> Map.put(:__struct__, to_string(report.__struct__))
|> Client.log_info(metadata)
end
defp log_info_report(report, metadata) do
report
|> Map.new()
|> Client.log_info(metadata)
end
defp log_error_report(report, metadata) when is_struct(report) do
report
|> Map.from_struct()
|> Map.put(:__struct__, to_string(report.__struct__))
|> Client.log_error(metadata)
end
defp log_error_report(report, metadata) do
case Map.new(report) do
%{reason: {exception, stacktrace}} when is_exception(exception) and is_list(stacktrace) ->
context = Map.put(Context.get(), :metadata, metadata)
error = Error.new(exception, stacktrace, context)
Client.send_error(error)
%{reason: {reason, stacktrace}} when is_list(stacktrace) ->
context = Map.put(Context.get(), :metadata, metadata)
error = Error.new(reason, stacktrace, context)
Client.send_error(error)
%{reason: reason} ->
Client.log_error(reason, metadata)
_ ->
Client.log_error(report, metadata)
end
end
end