How to Monitor Ex_Audit with Vigilmon
Ex_Audit hooks into Ecto's callbacks to automatically record every create, update, and delete on schemas you opt into. You get a configurable audit table, diff tracking, and optional custom metadata — all without changing your mutation call sites. The downside is that it's opt-in per schema and largely invisible when it's working, which means it's equally invisible when it stops.
This tutorial wires up monitoring to catch Ex_Audit failures before they become compliance gaps:
- HTTP health endpoint verifying the audit table is writable
- Heartbeat confirming audit records are being created at the expected rate
- Diff sanity check catching serialization failures
- Vigilmon uptime and heartbeat monitors
Why Monitor Ex_Audit?
| Signal | What it catches | |---|---| | Audit table availability | Schema migration dropped the audit table or its indexes | | Record write rate | Ex_Audit module removed from a schema without noticing | | Diff integrity | Changeset serialization errors producing null patches | | Custom metadata | Missing context (user, IP, request ID) in audit records | | Table size | Unbounded audit growth impacting query plans |
Ex_Audit doesn't raise by default when auditing fails in some configurations. External monitoring is the safety net.
Step 1: Set Up Ex_Audit
If you haven't already, configure Ex_Audit in config.exs:
config :ex_audit,
ecto_repos: [MyApp.Repo],
tracked_schemas: [MyApp.User, MyApp.Order]
And add use ExAudit.Schema to the schemas you want tracked:
defmodule MyApp.User do
use Ecto.Schema
use ExAudit.Schema
schema "users" do
field :email, :string
field :name, :string
timestamps()
end
end
Step 2: Add a Health Check
Create a module that queries the Ex_Audit versions table:
# lib/my_app/ex_audit_health.ex
defmodule MyApp.ExAuditHealth do
alias MyApp.Repo
import Ecto.Query
# Default Ex_Audit table — adjust if you configured a custom table name
@audit_table "versions"
def check do
since = DateTime.utc_now() |> DateTime.add(-300, :second)
result =
from(v in @audit_table,
where: v.recorded_at >= ^since,
select: %{
count: count(v.id),
null_patch_count: sum(fragment("CASE WHEN patch IS NULL THEN 1 ELSE 0 END"))
}
)
|> Repo.one()
case result do
%{count: count, null_patch_count: nulls} ->
{:ok, %{
audit_records_last_5min: count,
null_patches: nulls || 0
}}
nil ->
{:error, :query_failed}
end
rescue
e -> {:error, Exception.message(e)}
end
end
Step 3: Expose a Health Endpoint
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def ex_audit(conn, _params) do
case MyApp.ExAuditHealth.check() do
{:ok, %{null_patches: 0} = info} ->
json(conn, Map.put(info, :status, "ok"))
{:ok, info} ->
conn
|> put_status(503)
|> json(Map.put(info, :status, "degraded"))
{:error, reason} ->
conn
|> put_status(503)
|> json(%{status: "error", reason: reason})
end
end
end
Add the route:
scope "/health" do
get "/ex-audit", HealthController, :ex_audit
end
Test it:
curl http://localhost:4000/health/ex-audit
# => {"status":"ok","audit_records_last_5min":8,"null_patches":0}
Step 4: Add Vigilmon Uptime Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your URL:
https://yourapp.com/health/ex-audit. - Set Check interval to
2 minutes. - Set Expected HTTP status to
200. - Under Advanced, add a Response body contains check:
"status":"ok". - Click Save.
Step 5: Heartbeat for Audit Write Rate
The HTTP check only confirms the table is reachable. A separate heartbeat guards against the scenario where your schemas stop writing audit records (e.g., a schema migration removed use ExAudit.Schema):
# lib/my_app/ex_audit_heartbeat.ex
defmodule MyApp.ExAuditHeartbeat do
use GenServer
alias MyApp.Repo
import Ecto.Query
@interval :timer.minutes(5)
@heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
@min_records 1
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
schedule()
{:ok, nil}
end
def handle_info(:check, state) do
if audit_records_present?() do
HTTPoison.get!(@heartbeat_url)
end
schedule()
{:noreply, state}
end
defp audit_records_present? do
since = DateTime.utc_now() |> DateTime.add(-300, :second)
count =
from(v in "versions",
where: v.recorded_at >= ^since,
select: count(v.id)
)
|> Repo.one()
is_integer(count) and count >= @min_records
end
defp schedule, do: Process.send_after(self(), :check, @interval)
end
Add to your supervision tree and create the Vigilmon heartbeat:
- Click Add Monitor → Cron / Heartbeat.
- Set expected interval to
5 minutes. - Paste the generated URL into
@heartbeat_url.
Step 6: Track Custom Metadata Coverage
Ex_Audit supports custom metadata passed via ExAudit.track/1. If your call sites are supposed to pass user context but aren't, audit records lose traceability. Add a metadata coverage check:
# lib/my_app/ex_audit_metadata_check.ex
defmodule MyApp.ExAuditMetadataCheck do
alias MyApp.Repo
import Ecto.Query
def missing_user_context_last_hour do
since = DateTime.utc_now() |> DateTime.add(-3600, :second)
from(v in "versions",
where: v.recorded_at >= ^since,
where: is_nil(fragment("meta->>'user_id'")),
select: count(v.id)
)
|> Repo.one()
end
end
Wire this into a heartbeat that goes silent when missing_user_context_last_hour() > 0.
Step 7: Set Up Alerts
In Vigilmon:
- Go to Alert Channels → Add Channel.
- Configure Slack, email, or PagerDuty.
- Set Notify after to
2 consecutive failures. - Assign to:
- The HTTP monitor on
/health/ex-audit - The audit write-rate heartbeat
- The metadata coverage heartbeat (if created)
- The HTTP monitor on
Key Metrics to Watch
| Metric | Vigilmon feature | Alert threshold | |---|---|---| | Audit table availability | HTTP monitor | Any 5xx or timeout | | Audit write rate | Heartbeat | Missing for > 6 minutes | | Null patch rate | HTTP monitor body check | Any non-zero null_patches | | User context coverage | Heartbeat | Missing for > 1 heartbeat | | SSL certificate | Cert expiry monitor | Expires in < 14 days |
Conclusion
Ex_Audit's strength is its automatic tracking with zero call-site changes — but that same transparency makes failures invisible without external monitoring. Vigilmon's HTTP monitor catches database failures, the write-rate heartbeat detects silently removed schema hooks, and the null-patch check catches serialization regressions. Together they keep your audit trail trustworthy.
Get started free at vigilmon.online.