1defmodule Plug.RequestId do
2  @moduledoc """
3  A plug for generating a unique request id for each request. A generated
4  request id will in the format "uq8hs30oafhj5vve8ji5pmp7mtopc08f".
5
6  If a request id already exists as the "x-request-id" HTTP request header,
7  then that value will be used assuming it is between 20 and 200 characters.
8  If it is not, a new request id will be generated.
9
10  The request id is added to the Logger metadata as `:request_id` and the response as
11  the "x-request-id" HTTP header. To see the request id in your log output,
12  configure your logger backends to include the `:request_id` metadata:
13
14      config :logger, :console, metadata: [:request_id]
15
16  It is recommended to include this metadata configuration in your production
17  configuration file.
18
19  To use it, just plug it into the desired module:
20
21      plug Plug.RequestId
22
23  ## Options
24
25    * `:http_header` - The name of the HTTP *request* header to check for
26      existing request ids. This is also the HTTP *response* header that will be
27      set with the request id. Default value is "x-request-id"
28
29        plug Plug.RequestId, http_header: "custom-request-id"
30  """
31
32  require Logger
33  alias Plug.Conn
34  @behaviour Plug
35
36  def init(opts) do
37    Keyword.get(opts, :http_header, "x-request-id")
38  end
39
40  def call(conn, req_id_header) do
41    conn
42    |> get_request_id(req_id_header)
43    |> set_request_id(req_id_header)
44  end
45
46  defp get_request_id(conn, header) do
47    case Conn.get_req_header(conn, header) do
48      []      -> {conn, generate_request_id()}
49      [val|_] -> if valid_request_id?(val), do: {conn, val}, else: {conn, generate_request_id()}
50    end
51  end
52
53  defp set_request_id({conn, request_id}, header) do
54    Logger.metadata(request_id: request_id)
55    Conn.put_resp_header(conn, header, request_id)
56  end
57
58  defp generate_request_id do
59    Base.hex_encode32(:crypto.strong_rand_bytes(20), case: :lower)
60  end
61
62  defp valid_request_id?(s), do: byte_size(s) in 20..200
63end
64