102 lines
2.9 KiB
Elixir
102 lines
2.9 KiB
Elixir
defmodule MoundHuntersWeb.Plugs.RequestLogger do
|
|
@moduledoc """
|
|
Logs each HTTP request as a single JSON line in combined log format plus custom fields.
|
|
|
|
Log format includes:
|
|
- Standard combined log fields: remote_ip, timestamp, method, path, status, bytes, referer, user_agent
|
|
- Custom fields: request_id, duration_ms, query_params, lat, lng, tile_id, lookup_id, error
|
|
"""
|
|
import Plug.Conn
|
|
require Logger
|
|
|
|
def init(opts), do: opts
|
|
|
|
def call(conn, _opts) do
|
|
start_time = System.monotonic_time(:microsecond)
|
|
request_id = generate_request_id()
|
|
|
|
conn =
|
|
conn
|
|
|> put_private(:request_start_time, start_time)
|
|
|> put_private(:request_id, request_id)
|
|
|
|
register_before_send(conn, fn conn ->
|
|
log_request(conn, start_time, request_id)
|
|
conn
|
|
end)
|
|
end
|
|
|
|
defp log_request(conn, start_time, request_id) do
|
|
end_time = System.monotonic_time(:microsecond)
|
|
duration_ms = (end_time - start_time) / 1000.0
|
|
|
|
log_entry = %{
|
|
# Standard combined log format fields
|
|
remote_ip: format_remote_ip(conn.remote_ip),
|
|
timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
|
|
method: conn.method,
|
|
path: conn.request_path,
|
|
query_string: conn.query_string,
|
|
status: conn.status,
|
|
bytes_sent: get_resp_header(conn, "content-length") |> List.first() || "-",
|
|
referer: get_req_header(conn, "referer") |> List.first() || "-",
|
|
user_agent: get_req_header(conn, "user-agent") |> List.first() || "-",
|
|
|
|
# Custom fields
|
|
request_id: request_id,
|
|
duration_ms: Float.round(duration_ms, 2),
|
|
|
|
# Tile-specific fields (if present)
|
|
lat: get_query_param(conn, "lat"),
|
|
lng: get_query_param(conn, "lng"),
|
|
tile_id: get_private_field(conn, :tile_id),
|
|
lookup_id: get_private_field(conn, :lookup_id),
|
|
|
|
# Error information
|
|
error: get_private_field(conn, :error_message)
|
|
}
|
|
|
|
# Remove nil values for cleaner logs
|
|
log_entry = Enum.reject(log_entry, fn {_k, v} -> is_nil(v) end) |> Map.new()
|
|
|
|
# Log to file as JSON line
|
|
json_line = Jason.encode!(log_entry)
|
|
Logger.info(json_line, logger: :request_log)
|
|
|
|
# Also log summary to console
|
|
console_msg =
|
|
"#{conn.method} #{conn.request_path} - #{conn.status} - #{Float.round(duration_ms, 2)}ms"
|
|
Logger.info(console_msg)
|
|
end
|
|
|
|
defp format_remote_ip({a, b, c, d}) do
|
|
"#{a}.#{b}.#{c}.#{d}"
|
|
end
|
|
|
|
defp format_remote_ip({a, b, c, d, e, f, g, h}) do
|
|
parts = [a, b, c, d, e, f, g, h]
|
|
parts
|
|
|> Enum.map(&Integer.to_string(&1, 16))
|
|
|> Enum.join(":")
|
|
end
|
|
|
|
defp get_query_param(conn, key) do
|
|
case conn.query_params do
|
|
%{^key => value} when value != "" -> value
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp get_private_field(conn, key) do
|
|
case conn.private do
|
|
%{^key => value} -> value
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp generate_request_id do
|
|
:crypto.strong_rand_bytes(8)
|
|
|> Base.url_encode64(padding: false)
|
|
end
|
|
end
|