defmodule IntoLog do
defstruct [:log_fn]
import Logger
@moduledoc """
Collectable protocol for Logging, allowing a logger to be
passed anywhere a Collectable is accepted.
require IntoLog
System.cmd("echo", ["hello"], into: IntoLog.log(:info))
09:24:42.866 [info] hello
"""
@doc """
Returns a Collectable that logs each message with the given
`level` and metadata.
Enum.into(["hello", "world"], IntoLog.log(:info))
"""
defmacro log(level, metadata \\ []) do
macro_log(level, metadata, __CALLER__)
end
defp macro_log(level, metadata, caller) do
{maybe_application, file} = compile_time_application_and_file(caller)
location =
case caller do
%{module: module, function: {fun, arity}, line: line} ->
%{mfa: {module, fun, arity}, file: file, line: line}
_ ->
%{}
end
{compile_metadata, quoted_metadata} =
if Keyword.keyword?(metadata) do
metadata = Keyword.merge(maybe_application, metadata)
{Map.merge(location, Map.new(metadata)), escape_metadata(metadata)}
else
{%{},
quote do
Enum.into(unquote(metadata), unquote(escape_metadata(maybe_application)))
end}
end
compile_level = if is_atom(level), do: level, else: :error
if compile_time_purge_matching?(compile_level, compile_metadata) do
:ok
else
quote do
case Logger.__should_log__(unquote(level), __MODULE__) do
nil ->
%IntoLog{log_fn: fn _data -> nil end}
level ->
%IntoLog{
log_fn: fn data ->
Logger.__do_log__(
level,
data,
unquote(Macro.escape(location)),
unquote(quoted_metadata)
)
end
}
end
end
end
end
defp escape_metadata(metadata) do
{_, metadata} =
Keyword.get_and_update(metadata, :mfa, fn
nil -> :pop
mfa -> {mfa, Macro.escape(mfa)}
end)
{:%{}, [], metadata}
end
defp compile_time_application_and_file(%{file: file}) do
if app = Application.get_env(:logger, :compile_time_application) do
{[application: app], file |> Path.relative_to_cwd() |> String.to_charlist()}
else
{[], file |> Path.relative_to_cwd() |> String.to_charlist()}
end
end
defp compile_time_purge_matching?(level, compile_metadata) do
matching = Application.get_env(:logger, :compile_time_purge_matching, [])
if not is_list(matching) do
bad_compile_time_purge_matching!(matching)
end
Enum.any?(matching, fn filter ->
if not is_list(filter) do
bad_compile_time_purge_matching!(matching)
end
Enum.all?(filter, fn
{:level_lower_than, min_level} ->
compare_levels(level, min_level) == :lt
{:module, module} ->
match?({:ok, {^module, _, _}}, Map.fetch(compile_metadata, :mfa))
{:function, func} ->
case Map.fetch(compile_metadata, :mfa) do
{:ok, {_, f, a}} -> "#{f}/#{a}" == func
_ -> false
end
{k, v} when is_atom(k) ->
Map.fetch(compile_metadata, k) == {:ok, v}
_ ->
bad_compile_time_purge_matching!(matching)
end)
end)
end
defp bad_compile_time_purge_matching!(matching) do
raise "expected :compile_time_purge_matching to be a list of keyword lists, " <>
"got: #{inspect(matching)}"
end
end
defimpl Collectable, for: IntoLog do
require Logger
def into(log) do
{log,
fn
log_acc, {:cont, elem} ->
log_acc.log_fn.(elem)
log_acc
log_acc, :done ->
log_acc
_log_acc, :halt ->
:not_used
end}
end
end