Packages

A wrapper for Elixir Logger that keeps and returns the logger metadata from the caller processes.

Current section

Files

Jump to
meta_logger lib meta_logger.ex
Raw

lib/meta_logger.ex

defmodule MetaLogger do
@moduledoc """
A logger wrapper to keep logger metadata of caller processes.
"""
require Logger
@metadata ~w(dictionary $logger_metadata$)a
@type chardata_or_fun :: IO.chardata() | String.Chars.t() | (any() -> any())
@type metadata :: keyword()
@type payload :: struct() | list() | chardata_or_fun()
Enum.each(~w(debug error info warn)a, fn level ->
@doc """
Logs a #{level} message keeping logger metadata from caller processes.
Returns `:ok` or an `{:error, reason}` tuple.
## Examples
iex> #{inspect(__MODULE__)}.#{level}("hello?")
:ok
iex> #{inspect(__MODULE__)}.#{level}(fn -> "dynamically calculated debug" end)
:ok
iex> #{inspect(__MODULE__)}.#{level}(fn -> {"dynamically calculated #{level}", [additional: :metadata]} end)
:ok
"""
def unquote(level)(chardata_or_fun, metadata \\ []) do
set_logger_metadata_from_parent_processes()
Logger.unquote(level)(chardata_or_fun, metadata)
end
end)
@doc """
Logs a message with given `level` keeping logger metadata from caller processes.
Can accept a custom struct if it implements MetaLogger.Formatter protocol.
Returns `:ok` or an `{:error, reason}` tuple.
## Examples
iex> #{inspect(__MODULE__)}.log(:info, "mission accomplished")
:ok
iex> #{inspect(__MODULE__)}.log(:error, fn -> "dynamically calculated info" end)
:ok
iex> #{inspect(__MODULE__)}.log(:warn, fn -> {"dynamically calculated info", [additional: :metadata]} end)
:ok
"""
@spec log(atom(), payload(), metadata()) :: :ok
def log(level, payload, metadata \\ [])
def log(level, data_struct, metadata) when is_struct(data_struct) do
formatted_log = MetaLogger.Formatter.format(data_struct)
log(level, formatted_log, metadata)
end
def log(level, logs, metadata) when is_atom(level) and is_list(logs),
do: Enum.each(logs, &log(level, &1, metadata))
def log(level, chardata_or_fun, metadata) when is_atom(level) do
set_logger_metadata_from_parent_processes()
Logger.log(level, chardata_or_fun, metadata)
end
@spec set_logger_metadata_from_parent_processes() :: :ok
defp set_logger_metadata_from_parent_processes, do: Logger.metadata(metadata())
@doc """
Returns the logger metadata from the current process and caller processes.
## Examples
iex> #{inspect(__MODULE__)}.metadata()
[]
iex> #{inspect(__MODULE__)}.metadata()
[metadata1: "value2", metadata2: "value2"]
"""
@spec metadata() :: metadata()
def metadata do
:"$callers"
|> Process.get()
|> List.wrap()
|> Enum.reduce(Logger.metadata(), &merge_logger_metadata_from_parent_process/2)
end
@spec merge_logger_metadata_from_parent_process(pid(), metadata()) :: metadata()
defp merge_logger_metadata_from_parent_process(parent_pid, merged_metadata) do
parent_pid
|> get_process_logger_metadata()
|> Keyword.merge(merged_metadata)
end
@spec get_process_logger_metadata(pid()) :: metadata()
defp get_process_logger_metadata(pid) do
pid
|> Process.info()
|> get_in(@metadata)
|> case do
process_metadata when is_map(process_metadata) -> Map.to_list(process_metadata)
nil -> []
end
end
end