defmodule ToweropsWeb.Plugs.RemoteIpLogger do @moduledoc """ Plug that extracts the remote IP address and adds it to Logger metadata. When behind a proxy/load balancer (like Traefik in Kubernetes), the real client IP is in the X-Forwarded-For or X-Real-IP headers. This plug extracts the IP and adds it to Logger metadata so it appears in all logs for the request. """ import Plug.Conn require Logger def init(opts), do: opts def call(conn, _opts) do remote_ip = get_remote_ip(conn) # Add to Logger metadata so it appears in all logs for this request Logger.metadata(remote_ip: remote_ip) conn end defp get_remote_ip(conn) do # Try X-Forwarded-For first (set by Traefik and other proxies) case get_req_header(conn, "x-forwarded-for") do [forwarded | _] -> # X-Forwarded-For can be a comma-separated list; take the first (original client) forwarded |> String.split(",") |> List.first() |> String.trim() [] -> get_fallback_ip(conn) end end defp get_fallback_ip(conn) do # Try X-Real-IP header case get_req_header(conn, "x-real-ip") do [real_ip | _] -> real_ip [] -> format_remote_ip(conn.remote_ip) end end defp format_remote_ip({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}" defp format_remote_ip({a, b, c, d, e, f, g, h}), do: format_ipv6({a, b, c, d, e, f, g, h}) defp format_ipv6({a, b, c, d, e, f, g, h}) do Enum.map_join([a, b, c, d, e, f, g, h], ":", &Integer.to_string(&1, 16)) end end