defmodule Maxwell.Middleware.Logger do
@moduledoc """
Log the request and response by Logger, default log_level is :info.
Setting log_level in 3 ways:
### Log everything by log_level
middleware Maxwell.Middleware.Logger, log_level: :debug
### Log request by specific status code.
middleware Maxwell.Middleware.Logger, log_level: [debug: 200, error: 404, info: default]
### Log request by status code's Ranges
middleware Maxwell.Middleware.Logger, log_level: [error: [500..599, 300..399, 400], warn: 404, debug: default]
### Examples
# Client.ex
use Maxwell.Builder ~(get)a
middleware Maxwell.Middleware.Logger, log_level: [
info: [1..100, 200..299, 404],
warn: 300..399,
error: :default
]
def your_own_request(url) do
url |> new() |> get!()
end
"""
use Maxwell.Middleware
require Logger
@levels [:info, :debug, :warn, :error]
def init(opts) do
case Keyword.pop(opts, :log_level) do
{_, [_ | _]} ->
raise ArgumentError, "Logger Middleware Options doesn't accept wrong_option (:log_level)"
{nil, _} ->
[default: :info]
{options, _} when is_list(options) ->
parse_opts(options)
{level, _} ->
parse_opts([{level, :default}])
end
end
def call(request_env, next_fn, options) do
start = System.system_time(:millisecond)
new_result = next_fn.(request_env)
case new_result do
{:error, reason, _conn} ->
method = request_env.method |> to_string |> String.upcase()
Logger.error("#{method} #{request_env.url}>> #{IO.ANSI.red()}ERROR: #{inspect(reason)}")
%Maxwell.Conn{} = response_conn ->
stop = System.system_time(:millisecond)
diff = stop - start
log_response_message(options, response_conn, diff)
end
new_result
end
defp log_response_message(options, conn, diff) do
%Maxwell.Conn{status: status, url: url, method: method} = conn
level = get_level(options, status)
color =
case level do
nil -> nil
:debug -> IO.ANSI.cyan()
:info -> IO.ANSI.normal()
:warn -> IO.ANSI.yellow()
:error -> IO.ANSI.red()
end
unless is_nil(level) do
message =
"#{method} #{url} <<<#{color}#{status}(#{diff}ms)#{IO.ANSI.reset()}\n#{inspect(conn)}"
Logger.log(level, message)
end
end
defp get_level([], _code), do: nil
defp get_level([{code, level} | _], code), do: level
defp get_level([{from..to, level} | _], code)
when code in from..to,
do: level
defp get_level([{:default, level} | _], _code), do: level
defp get_level([_ | t], code), do: get_level(t, code)
defp parse_opts(options), do: parse_opts(options, [], nil)
defp parse_opts([], result, nil), do: Enum.reverse(result)
defp parse_opts([], result, default), do: Enum.reverse([{:default, default} | result])
defp parse_opts([{level, :default} | rest], result, nil) do
check_level(level)
parse_opts(rest, result, level)
end
defp parse_opts([{level, :default} | rest], result, level) do
Logger.warn("Logger Middleware: default level defined multiple times.")
parse_opts(rest, result, level)
end
defp parse_opts([{_level, :default} | _rest], _result, _default) do
raise ArgumentError, "Logger Middleware: default level conflict."
end
defp parse_opts([{level, codes} | rest], result, default) when is_list(codes) do
check_level(level)
result =
Enum.reduce(codes, result, fn code, acc ->
check_code(code)
[{code, level} | acc]
end)
parse_opts(rest, result, default)
end
defp parse_opts([{level, code} | rest], result, default) do
check_level(level)
check_code(code)
parse_opts(rest, [{code, level} | result], default)
end
defp check_level(level) when level in @levels, do: :ok
defp check_level(_level) do
raise ArgumentError, "Logger Middleware: level only accepts #{inspect(@levels)}."
end
defp check_code(code) when is_integer(code), do: :ok
defp check_code(_from.._to), do: :ok
defp check_code(_any) do
raise ArgumentError, "Logger Middleware: status code only accepts Integer and Range."
end
end