prop/lib/microwaveprop_web/plugs/remote_ip.ex
Graham McIntire fbbb8b2e1e
feat(logging): add method + request_path to log metadata
Every Sent log line now carries method and path so noisy traffic
(probes, scrapers, redirects) is identifiable without a tracer.
RemoteIp plug already sets remote_ip metadata; add method and
request_path alongside it and register the extra keys with the
default formatter.
2026-04-21 09:07:31 -05:00

59 lines
1.3 KiB
Elixir

defmodule MicrowavepropWeb.Plugs.RemoteIp do
@moduledoc """
Extracts the client IP from X-Forwarded-For (set by nginx/dokku proxy)
and assigns it to conn.remote_ip and Logger metadata.
"""
@behaviour Plug
@impl true
def init(opts), do: opts
@impl true
# Cloudflare sets Cf-Connecting-Ip with the real client IP.
# Fall back to X-Forwarded-For for non-tunneled requests (dev, direct).
@ip_headers ["cf-connecting-ip", "x-forwarded-for"]
def call(conn, _opts) do
ip =
Enum.find_value(@ip_headers, fn header ->
case Plug.Conn.get_req_header(conn, header) do
[value | _] ->
value
|> String.split(",")
|> hd()
|> String.trim()
|> parse_ip()
_ ->
nil
end
end)
conn =
if ip do
%{conn | remote_ip: ip}
else
conn
end
Logger.metadata(
remote_ip: format_ip(conn.remote_ip),
method: conn.method,
request_path: conn.request_path
)
conn
end
defp parse_ip(str) do
case :inet.parse_address(String.to_charlist(str)) do
{:ok, ip} -> ip
_ -> nil
end
end
defp format_ip({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}"
defp format_ip({a, b, c, d, e, f, g, h}), do: "#{a}:#{b}:#{c}:#{d}:#{e}:#{f}:#{g}:#{h}"
defp format_ip(_), do: "unknown"
end