create backend
This commit is contained in:
101
lib/mound_hunters_web/plugs/request_logger.ex
Normal file
101
lib/mound_hunters_web/plugs/request_logger.ex
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
Reference in New Issue
Block a user