Current section

Files

Jump to
prima_ex_logger lib prima_ex_logger.ex
Raw

lib/prima_ex_logger.ex

defmodule PrimaExLogger do
@moduledoc """
Custom logger to send json over stdout
"""
@behaviour :gen_event
@ignored_metadata_keys ~w[ansi_color pid]a
@dialyzer {:nowarn_function,
[init: 1, configure: 2, forge_event: 2, timestamp_to_iso: 1, log: 2]}
@spec init({PrimaExLogger, atom()}) :: {:error, any()} | {:ok, any()} | {:ok, any(), :hibernate}
def init({__MODULE__, name}) do
{:ok, configure(name, [])}
end
def handle_call({:configure, opts}, %{name: name}) do
{:ok, :ok, configure(name, opts)}
end
@spec configure(atom(), Keyword.t()) :: map()
defp configure(name, opts) do
env = Application.get_env(:logger, name, [])
opts = Keyword.merge(env, opts)
Application.put_env(:logger, name, opts)
level = Keyword.get(opts, :level, :info)
encoder = Keyword.get(opts, :encoder, Jason)
environment = Keyword.get(opts, :environment, nil)
type = Keyword.get(opts, :type, nil)
metadata = Keyword.get(opts, :metadata, []) |> configure_metadata()
metadata_serializers = Keyword.get(opts, :metadata_serializers, [])
%{
level: level,
name: name,
encoder: encoder,
type: type,
environment: environment,
metadata: metadata,
metadata_serializers: metadata_serializers
}
end
def handle_event({level, _, _} = event, %{level: min_level, encoder: encoder} = state) do
case Logger.compare_levels(level, min_level) do
:lt ->
nil
_ ->
event
|> forge_event(state)
|> log(encoder)
end
{:ok, state}
end
def handle_event(:flush, state) do
{:ok, state}
end
def handle_info(_, state) do
{:ok, state}
end
def code_change(_old_vsn, state, _extra) do
{:ok, state}
end
def terminate(_reason, _state) do
:ok
end
@spec forge_event(tuple(), map()) :: map()
defp forge_event({level, _, {Logger, message, timestamp, metadata}}, %{
type: type,
environment: environment,
metadata: fields,
metadata_serializers: custom_serializers
}) do
%{
"message" => IO.iodata_to_binary(message),
"level" => level,
"type" => type,
"environment" => environment,
"metadata" => take_metadata(metadata, fields, custom_serializers),
"timestamp" => timestamp_to_iso(timestamp)
}
end
@spec take_metadata(list(), any(), list()) :: map()
defp take_metadata(metadata, fields, custom_serializers) do
metadata
|> Keyword.merge(fields)
|> Keyword.drop(@ignored_metadata_keys)
|> to_printable(custom_serializers)
end
@spec to_printable(any(), list()) :: any()
def to_printable(v, _) when is_binary(v), do: v
def to_printable(v, _) when is_atom(v), do: v
def to_printable(v, _) when is_number(v), do: v
def to_printable(v, custom_serializers) when is_list(v) do
if Keyword.keyword?(v) do
v
|> Enum.into(%{})
|> to_printable(custom_serializers)
else
Enum.map(v, &to_printable(&1, custom_serializers))
end
end
def to_printable(%t{} = v, custom_serializers) when t in [Date, DateTime, NaiveDateTime],
do: to_printable(inspect(v), custom_serializers)
def to_printable(%t{} = v, custom_serializers) do
custom_serializers
|> Enum.find(fn
{^t, _} -> true
_ -> false
end)
|> case do
nil ->
to_printable(Map.from_struct(v), custom_serializers)
{_module, fun} when is_function(fun, 1) ->
fun
|> apply([v])
|> to_printable(custom_serializers)
{module, fun} when is_atom(fun) ->
module
|> apply(fun, [v])
|> to_printable(custom_serializers)
end
end
def to_printable(v, custom_serializers) when is_map(v) do
Enum.into(v, %{}, fn {k, v} ->
{to_printable(k, custom_serializers), to_printable(v, custom_serializers)}
end)
end
def to_printable(v, _), do: inspect(v)
@spec timestamp_to_iso(tuple()) :: String.t()
def timestamp_to_iso({{year, month, day}, {hour, minute, second, milliseconds}}) do
case NaiveDateTime.new(
%Date{
year: year,
month: month,
day: day
},
%Time{
hour: hour,
minute: minute,
second: second,
microsecond: {1000 * milliseconds, 6}
}
) do
{:ok, ts} ->
ts
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_iso8601(:extended)
_ ->
nil
end
end
@spec log(map(), module()) :: :ok
defp log(event, encoder) do
case apply(encoder, :encode, [event]) do
{:ok, json} ->
IO.puts(json)
{:error, reason} ->
IO.puts(
"Error during JSON encoding. Reason: #{inspect(reason)}, event: #{inspect(event)}"
)
end
end
@spec configure_metadata(list() | atom()) :: list()
defp configure_metadata([]), do: []
defp configure_metadata(:all), do: []
defp configure_metadata(metadata) when is_list(metadata), do: Enum.reverse(metadata)
end