Current section

Files

Jump to
timber lib timber integrations ex_aws_http_client.ex
Raw

lib/timber/integrations/ex_aws_http_client.ex

defmodule Timber.Integrations.ExAwsHTTPClient do
@moduledoc """
[ExAWS](https://github.com/CargoSense/ex_aws) is an excellent library for interfacing with the
AWS API. The Timber `ExAWSHTTPClient` adds structured logging to the HTTP requests being
sent to the AWS API. This gives you valuable insight into AWS communication from your application.
We use is internally at Timber.
**By default**, this library will only log destructive requests (`POST`, `PUT`, `DELETE`,
and `PATCH`). `GET` requests can be turned via configuration (see below).
## Installation
```elixir
config :ex_aws,
http_client: Timber.Integrations.ExAwsHTTPClient
```
## Configuration
```elixir
config :timber, Timber.Integrations.ExAWSHTTPClient,
http_methods_to_log: [:post, :put, :delete, :patch],
capture_bodies: false
```
* `http_methods_to_log` - (default: `[:post, :put, :delete, :patch]`). Only log requests whose method
is included in the list. Use `:all` to log all methods.
* `capture_bodies` - (default: `false`). When enabled, The first 2048 characters of the body will
be included in the metadata of the log. It is recommended to enable this for debug purposes
only as it can get excessive. You can also lower the size via
`config :timber, :http_body_size_limit, 2048`.
## Only log specific services
ExAws offers the ability to configure on a per service basis:
```elixir
config :ex_aws, :lambda,
http_client: Timber.Integrations.ExAwsHTTPClient
```
Alternatively you can exclude services in the same way:
```elixir
config :ex_aws,
http_client: Timber.Integrations.ExAwsHTTPClient
# Fallback to the defualt, non-logging HTTP client
config :ex_aws, :lambda,
http_client: ExAws.Request.Hackney
```
"""
alias Timber.Events.HTTPRequestEvent
alias Timber.Events.HTTPResponseEvent
require Logger
# Set a timeout slightly over the general AWS timeout. This ensures that we receive
# the timeout event from AWS before we receive it internally, preventing orphaned requests.
@default_opts [recv_timeout: 62_000]
@default_service_name "aws"
@http_methods_to_log_default [:patch, :post, :put, :delete]
def request(method, url, body \\ "", headers \\ [], http_opts \\ []) do
opts =
:ex_aws
|> Application.get_env(:hackney_opts, @default_opts)
|> Keyword.merge(http_opts)
|> Keyword.put(:with_body, true)
service_name =
case String.split(url, ".", parts: 2) do
[prefix, _suffix] ->
String.replace(prefix, "https://", "")
_else ->
@default_service_name
end
timer = Timber.start_timer()
should_log = should_log_method?(method, http_methods_to_log())
log_request(should_log, service_name, method, url, body, headers)
case :hackney.request(method, url, headers, body, opts) do
{:ok, status, headers} ->
log_response(should_log, service_name, status, headers, timer)
{:ok, %{status_code: status, headers: headers}}
{:ok, status, headers, body} ->
log_response(should_log, service_name, status, headers, timer, body: body)
{:ok, %{status_code: status, headers: headers, body: body}}
{:error, reason} ->
# Errors are not logged because they should be handled. It is up
# to the caller to log these properly.
{:error, %{reason: reason}}
end
end
defp should_log_method?(_method, :all),
do: true
defp should_log_method?(method, allowed_methods) do
Enum.member?(allowed_methods, method)
end
defp log_request(false, _service_name, _method, _url, _body, _headers),
do: nil
defp log_request(true, service_name, method, url, body, headers) do
Logger.debug fn ->
body = if capture_bodies?(), do: body, else: nil
event =
HTTPRequestEvent.new(
direction: "outgoing",
method: method,
url: url,
body: body,
headers: headers,
service_name: service_name
)
message = HTTPRequestEvent.message(event)
{message, event: event}
end
end
defp log_response(should_log, service_name, status, headers, timer, opts \\ [])
defp log_response(false, _service_name, _status, _headers, _timer, _opts),
do: nil
defp log_response(true, service_name, status, headers, timer, opts) do
Logger.debug fn ->
time_ms = Timber.duration_ms(timer)
body = Keyword.get(opts, :body)
body = if capture_bodies?(), do: body, else: nil
event =
HTTPResponseEvent.new(
direction: "incoming",
body: body,
status: status,
headers: headers,
service_name: service_name,
time_ms: time_ms
)
message = HTTPResponseEvent.message(event)
{message, event: event}
end
end
#
# Config
#
defp config, do: Elixir.Application.get_env(:timber, __MODULE__, [])
defp capture_bodies?, do: Keyword.get(config(), :capture_bodies, false)
defp http_methods_to_log, do: Keyword.get(config(), :http_methods_to_log, @http_methods_to_log_default)
end