defmodule Plug.RequestId do
@moduledoc """
A plug for generating a unique request ID for each request.
The generated request ID will be in the format:
```
uq8hs30oafhj5vve8ji5pmp7mtopc08f
```
If a request ID already exists in a configured HTTP request header (see options below),
then this plug will use that value, *assuming it is between 20 and 200 characters*.
If such header is not present, this plug will generate a new request ID.
The request ID is added to the `Logger` metadata as `:request_id`, and to the
response as the configured HTTP response header (see options below). To see the
request ID in your log output, configure your logger backends to include the `:request_id`
metadata. For example:
config :logger, :console, metadata: [:request_id]
We recommend to include this metadata configuration in your production
configuration file.
> #### Programmatic access to the request ID {: .tip}
>
> To access the request ID programmatically, use the `:assign_as` option (see below)
> to assign the request ID to a key in `conn.assigns`, and then fetch it from there.
## Usage
To use this plug, just plug it into the desired module:
plug Plug.RequestId
## Options
* `:http_header` - The name of the HTTP *request* header to check for
existing request IDs. This is also the HTTP *response* header that will be
set with the request id. Default value is `"x-request-id"`.
plug Plug.RequestId, http_header: "custom-request-id"
* `:assign_as` - The name of the key that will be used to store the
discovered or generated request id in `conn.assigns`. If not provided,
the request id will not be stored. *Available since v1.16.0*.
plug Plug.RequestId, assign_as: :plug_request_id
"""
require Logger
alias Plug.Conn
@behaviour Plug
@impl true
def init(opts) do
{
Keyword.get(opts, :http_header, "x-request-id"),
Keyword.get(opts, :assign_as)
}
end
@impl true
def call(conn, {header, assign_as}) do
request_id = get_request_id(conn, header)
Logger.metadata(request_id: request_id)
conn = if assign_as, do: Conn.assign(conn, assign_as, request_id), else: conn
Conn.put_resp_header(conn, header, request_id)
end
defp get_request_id(conn, header) do
case Conn.get_req_header(conn, header) do
[] -> generate_request_id()
[val | _] -> if valid_request_id?(val), do: val, else: generate_request_id()
end
end
defp generate_request_id do
binary = <<
System.system_time(:nanosecond)::64,
:erlang.phash2({node(), self()}, 16_777_216)::24,
:erlang.unique_integer()::32
>>
Base.url_encode64(binary)
end
defp valid_request_id?(s), do: byte_size(s) in 20..200
end