defmodule Salemove.HttpClient.Middleware.Logger do
@behaviour Tesla.Middleware
@moduledoc """
Log requests as single line.
Logs request method, url, response status and time taken in milliseconds.
### Example usage
```
defmodule MyClient do
use Tesla
plug Salemove.HttpClient.Middleware.Logger,
level: %{
100..399 => :info,
422 => :info,
400..499 => :warning,
500..600 => :error
}
end
```
### Logger output
```
2017-09-30 13:39:06.663 [info] GET http://example.com -> 200 (736.988 ms)
```
"""
require Logger
def call(env, next, opts) do
time_start = System.monotonic_time()
with {:ok, env} <- Tesla.run(env, next) do
elapsed_ms = elapsed_ms(time_start)
log(env, elapsed_ms, metadata(env, elapsed_ms), opts)
{:ok, env}
else
{:error, error} ->
elapsed_ms = elapsed_ms(time_start)
log(env, error, elapsed_ms, metadata(env, elapsed_ms), opts)
{:error, error}
end
end
defp log(env, :timeout, elapsed_ms, metadata, _opts) do
message = "#{normalize_method(env)} #{env.url} -> :timeout (#{elapsed_ms} ms)"
Logger.log(:warning, message, metadata ++ [status: "timeout"])
end
defp log(env, :closed, elapsed_ms, metadata, _opts) do
message = "#{normalize_method(env)} #{env.url} -> :closed (#{elapsed_ms} ms)"
Logger.log(:warning, message, metadata ++ [status: "closed"])
end
defp log(env, %Tesla.Error{reason: reason}, elapsed_ms, metadata, opts) do
status = inspect(reason)
log_status(
0,
"#{normalize_method(env)} #{env.url} -> #{status} (#{elapsed_ms} ms)",
metadata ++ [status: normalize_status(status)],
opts
)
end
defp log(env, error, elapsed_ms, metadata, opts) do
status = inspect(error)
log_status(
0,
"#{normalize_method(env)} #{env.url} -> #{status} (#{elapsed_ms} ms)",
metadata ++ [status: normalize_status(status)],
opts
)
end
defp log(env, elapsed_ms, metadata, opts) do
message = "#{normalize_method(env)} #{env.url} -> #{env.status} (#{elapsed_ms} ms)"
log_status(env.status, message, metadata ++ [status: normalize_status(env.status)], opts)
end
defp normalize_method(env) do
env.method |> to_string() |> String.upcase()
end
defp normalize_status(status), do: to_string(status)
defp extract_path(%{url: url} = _env) do
%URI{path: path} = URI.parse(url)
path
end
defp elapsed_ms(from) do
now = System.monotonic_time()
us = System.convert_time_unit(now - from, :native, :microsecond)
:io_lib.format("~.3f", [us / 1000])
end
defp log_status(status, message, metadata, opts) do
levels = Keyword.get(opts || [], :level)
status
|> status_to_level(levels)
|> Logger.log(message, metadata)
end
defp status_to_level(status, levels) when is_map(levels) do
case levels do
%{^status => level} -> level
levels -> find_matching_level(levels, status) || status_to_level(status, nil)
end
end
defp status_to_level(status, _) do
cond do
status >= 500 -> :warning
status >= 400 || status == 0 -> :error
status >= 300 -> :warning
true -> :info
end
end
defp find_matching_level(levels, status) do
Enum.find_value(levels, fn
{%Range{} = range, level} -> if status in range, do: level
{^status, level} -> level
_ -> false
end)
end
defp metadata(env, elapsed_ms) do
[method: normalize_method(env), path: extract_path(env), duration_ms: to_string(elapsed_ms), url: env.url]
end
end