Current section
Files
Jump to
Current section
Files
lib/tesla/middleware/meta_logger.ex
if Code.ensure_loaded?(Tesla) do
defmodule Tesla.Middleware.MetaLogger do
@moduledoc """
Tesla middleware to log requests and responses.
You can pass the options to the middleware or to the request `Tesla.Env`,
env options takes precedence over middleware options.
## Example usage
defmodule MyClient do
use Tesla
plug #{inspect(__MODULE__)},
filter_headers: ["authorization"],
filter_query_params: [:api_key],
log_level: :debug,
log_tag: MyApp
end
## Options
* `:filter_headers` - The headers that should not be logged,
the values will be replaced with `[FILTERED]`, defaults to: `[]`.
* `:filter_query_params` - The query params that should not be logged,
the values will be replaced with `[FILTERED]`, defaults to: `[]`
* `:log_level` - The log level to be used, defaults to: `:info`. Responses with
HTTP status 400 and above will be logged with `:error`, and redirect with `:warn`.
* `:log_tag` - The log tag to be prefixed in the logs, default to: `#{inspect(__MODULE__)}`.
"""
require Logger
alias Tesla.{Env, Middleware}
@behaviour Middleware
@typep message :: String.t() | [String.t()] | nil | atom()
@empty_values [nil, ""]
@filtered "[FILTERED]"
@impl Middleware
def call(%Env{opts: opts} = env, next, options) do
options = prepare_options(options, opts)
env
|> log_request(options)
|> Tesla.run(next)
|> log_response(options)
end
@spec prepare_options(Env.opts(), Env.opts()) :: Env.opts()
defp prepare_options(middleware_options, env_options) do
middleware_options
|> Keyword.merge(env_options, fn _key, _middleware_value, env_value -> env_value end)
|> maybe_put_default_value(:filter_headers, [])
|> maybe_put_default_value(:filter_query_params, [])
|> maybe_put_default_value(:log_level, :info)
|> maybe_put_default_value(:log_tag, __MODULE__)
end
@spec maybe_put_default_value(Env.opts(), atom(), any()) :: Env.opts()
defp maybe_put_default_value(options, key, default_value),
do: Keyword.put(options, key, Keyword.get(options, key, default_value))
@spec log_request(Env.t(), Env.opts()) :: Env.t()
defp log_request(%Env{body: body, method: method, query: query, url: url} = env, options) do
level = Keyword.get(options, :log_level)
headers = build_headers(env, options)
query = build_query(query, options)
log([format_method(method), url, inspect(query), inspect(headers)], level, options)
log(body, level, options)
env
end
@spec log_response(Env.result(), Env.opts()) :: Env.result()
defp log_response({:ok, %Env{body: body, status: status} = env} = result, options) do
level = response_log_level(result, options)
headers = build_headers(env, options)
log([status, inspect(headers)], level, options)
log(body, level, options)
result
end
defp log_response({:error, reason} = result, options) do
level = response_log_level(result, options)
log(reason, level, options)
result
end
@spec build_headers(Env.t(), Env.opts()) :: Env.headers()
defp build_headers(%Env{headers: headers}, options) do
filter_headers = Keyword.get(options, :filter_headers)
Enum.map(headers, &filter_header(&1, filter_headers))
end
@spec filter_header({String.t(), String.t()}, [String.t()]) :: {String.t(), String.t()}
defp filter_header({key, _value} = header, filter_headers),
do: if(key in filter_headers, do: {key, @filtered}, else: header)
@spec build_query(Env.query(), Env.opts()) :: Env.query()
defp build_query(query, options) do
filter_query_params = Keyword.get(options, :filter_query_params)
Enum.map(query, &filter_query_params(&1, filter_query_params))
end
@spec filter_query_params({String.t() | atom(), String.t()}, [atom() | String.t()]) ::
{atom() | String.t(), String.t()}
defp filter_query_params({key, _value} = param, filter_query_params),
do: if(key in filter_query_params, do: {key, @filtered}, else: param)
@spec response_log_level(Env.result(), Env.opts()) :: Logger.level()
defp response_log_level({:error, _any}, _options), do: :error
defp response_log_level({:ok, %Env{status: status}}, _options) when status >= 400, do: :error
defp response_log_level({:ok, %Env{status: status}}, _options) when status >= 300, do: :warn
defp response_log_level(_result, options), do: Keyword.get(options, :log_level)
@spec format_method(atom()) :: String.t()
defp format_method(method) do
method
|> to_string()
|> String.upcase()
end
@spec log(message(), Logger.level(), Env.opts()) :: :ok
defp log(message, _level, _options) when message in @empty_values, do: :ok
defp log(message, level, options) when is_list(message) do
message
|> Enum.join(" ")
|> log(level, options)
end
defp log(message, level, options) when is_binary(message) do
tag =
options
|> Keyword.get(:log_tag)
|> inspect()
MetaLogger.log(level, Miss.String.build("[", tag, "] ", message))
end
defp log(message, level, options), do: log(inspect(message), level, options)
end
end