From 45fb9736fa98ff7c9c6e918520dba30a6155cbe0 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 31 Mar 2026 12:05:05 -0500 Subject: [PATCH] Log client IP from X-Forwarded-For in request logs Add RemoteIp plug that extracts real client IP from X-Forwarded-For header (set by nginx/dokku proxy) and adds it to Logger metadata. --- config/config.exs | 2 +- lib/microwaveprop_web/endpoint.ex | 1 + lib/microwaveprop_web/plugs/remote_ip.ex | 48 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 lib/microwaveprop_web/plugs/remote_ip.ex diff --git a/config/config.exs b/config/config.exs index dfb95bfc..b53f9e14 100644 --- a/config/config.exs +++ b/config/config.exs @@ -20,7 +20,7 @@ config :esbuild, # Configure Elixir's Logger config :logger, :default_formatter, format: "$time $metadata[$level] $message\n", - metadata: [:request_id] + metadata: [:request_id, :remote_ip] # Configure the mailer # diff --git a/lib/microwaveprop_web/endpoint.ex b/lib/microwaveprop_web/endpoint.ex index 4a0933c9..e4387b05 100644 --- a/lib/microwaveprop_web/endpoint.ex +++ b/lib/microwaveprop_web/endpoint.ex @@ -41,6 +41,7 @@ defmodule MicrowavepropWeb.Endpoint do cookie_key: "request_logger" plug Plug.RequestId + plug MicrowavepropWeb.Plugs.RemoteIp plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] plug Plug.Parsers, diff --git a/lib/microwaveprop_web/plugs/remote_ip.ex b/lib/microwaveprop_web/plugs/remote_ip.ex new file mode 100644 index 00000000..9cdef76e --- /dev/null +++ b/lib/microwaveprop_web/plugs/remote_ip.ex @@ -0,0 +1,48 @@ +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 + def call(conn, _opts) do + ip = + case Plug.Conn.get_req_header(conn, "x-forwarded-for") do + [forwarded | _] -> + forwarded + |> String.split(",") + |> hd() + |> String.trim() + |> parse_ip() + + _ -> + nil + end + + conn = + if ip do + %{conn | remote_ip: ip} + else + conn + end + + Logger.metadata(remote_ip: format_ip(conn.remote_ip)) + 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