Packages
Reusable structured audit logging, DB change tracking, and crash reporting for Elixir/Phoenix apps
Current section
Files
Jump to
Current section
Files
lib/audit_trail/plug.ex
defmodule AuditTrail.Plug do
@moduledoc """
Phoenix plug that sets the actor context and logs every HTTP request.
Add to your pipeline in router.ex:
pipeline :api do
plug :accepts, ["json"]
plug AuditTrail.Plug
end
The plug reads `conn.assigns.current_user` by default.
To use a custom extractor, configure:
config :audit_trail, actor_extractor: MyApp.AuditActorExtractor
The extractor module must export `extract(conn) :: map()`.
"""
@behaviour Plug
@impl Plug
def init(opts), do: opts
@impl Plug
def call(conn, _opts) do
start_time = System.monotonic_time(:millisecond)
# Set initial actor — may be anonymous before auth runs.
# Auth plugs must call AuditTrail.set_actor(user) after loading
# the user so the before_send hook picks up the real identity.
AuditTrail.ActorStore.set(extract_actor(conn))
Plug.Conn.register_before_send(conn, fn conn ->
duration_ms = System.monotonic_time(:millisecond) - start_time
actor = AuditTrail.ActorStore.get()
AuditTrail.Logger.emit("request", %{
method: conn.method,
path: conn.request_path,
status: conn.status,
duration_ms: duration_ms,
actor_id: actor.id,
actor_name: actor.name,
actor_email: actor.email,
ip: format_ip(conn.remote_ip),
user_agent: get_header(conn, "user-agent"),
request_id: get_header(conn, "x-request-id"),
query_string: conn.query_string
})
conn
end)
end
defp extract_actor(conn) do
case AuditTrail.Config.actor_extractor() do
nil -> from_assigns(conn)
module -> safe_extract(module, conn)
end
end
defp from_assigns(conn) do
user = conn.assigns[:current_user]
if is_nil(user) do
%{id: "anonymous", name: "anonymous", email: nil}
else
%{
id: to_string(Map.get(user, :id, "anonymous")),
name:
Map.get(user, :username) || Map.get(user, :name) || Map.get(user, :email) || "unknown",
email: Map.get(user, :email)
}
end
end
defp safe_extract(module, conn) do
module.extract(conn)
rescue
_ -> %{id: "anonymous", name: "anonymous", email: nil}
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] |> Enum.map(&Integer.to_string(&1, 16)) |> Enum.join(":")
defp format_ip(_), do: "unknown"
defp get_header(conn, key) do
case Plug.Conn.get_req_header(conn, key) do
[value | _] -> value
[] -> "unknown"
end
end
end