How to Monitor PaperTrail (Ecto) with Vigilmon
PaperTrail gives your Ecto schemas a full version history: every insert, update, and delete creates a PaperTrail.Version record with a JSON diff, a timestamp, and optional actor metadata. That history is often what legal, compliance, or support teams reach for first — which means when it breaks, you need to know immediately, not when someone opens a ticket.
This tutorial adds monitoring for the PaperTrail version pipeline:
- HTTP health endpoint confirming the versions table is writable
- Heartbeat tracking that new versions are being created at the expected rate
- Actor context coverage check for compliance
- Vigilmon uptime and heartbeat monitors
Why Monitor PaperTrail?
| Signal | What it catches |
|---|---|
| Version table availability | Database connection or schema migration failures |
| Write rate | Application code no longer calling PaperTrail.insert/update/delete |
| Actor coverage | Missing originator metadata in version records |
| Diff accuracy | Serialization errors producing null or empty diffs |
| Table growth | Version table bloat degrading query performance |
PaperTrail is passive — it doesn't raise if writes fail in silent configurations. External monitoring is the only way to know when the trail goes cold.
Step 1: Add a Health Check
Create a module that queries the versions table and confirms it is reachable:
# lib/my_app/paper_trail_health.ex
defmodule MyApp.PaperTrailHealth do
alias MyApp.Repo
import Ecto.Query
def check do
since = DateTime.utc_now() |> DateTime.add(-300, :second)
count =
from(v in PaperTrail.Version,
where: v.inserted_at >= ^since,
select: count(v.id)
)
|> Repo.one()
{:ok, %{versions_last_5min: count}}
rescue
e -> {:error, Exception.message(e)}
end
end
Step 2: Expose a Health Endpoint
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def paper_trail(conn, _params) do
case MyApp.PaperTrailHealth.check() do
{:ok, info} ->
json(conn, Map.put(info, :status, "ok"))
{:error, reason} ->
conn
|> put_status(503)
|> json(%{status: "error", reason: reason})
end
end
end
Add the route:
# lib/my_app_web/router.ex
scope "/health" do
get "/paper-trail", HealthController, :paper_trail
end
Test it:
curl http://localhost:4000/health/paper-trail
# => {"status":"ok","versions_last_5min":15}
Step 3: 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/paper-trail. - 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 4: Heartbeat for Version Write Rate
If the application stops calling PaperTrail.insert/2 / PaperTrail.update/2, the version count drops to zero but the health endpoint still returns 200. A heartbeat guard catches this:
# lib/my_app/paper_trail_heartbeat.ex
defmodule MyApp.PaperTrailHeartbeat do
use GenServer
alias MyApp.Repo
import Ecto.Query
@interval :timer.minutes(10)
@heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
# Tune to your expected minimum write rate over 10 minutes
@min_versions_per_interval 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 versions_flowing?() do
HTTPoison.get!(@heartbeat_url)
end
schedule()
{:noreply, state}
end
defp versions_flowing? do
since = DateTime.utc_now() |> DateTime.add(-600, :second)
count =
from(v in PaperTrail.Version,
where: v.inserted_at >= ^since,
select: count(v.id)
)
|> Repo.one()
is_integer(count) and count >= @min_versions_per_interval
end
defp schedule, do: Process.send_after(self(), :check, @interval)
end
Add to your supervision tree:
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.PaperTrailHeartbeat
]
Create the Vigilmon heartbeat monitor:
- Click Add Monitor → Cron / Heartbeat.
- Set expected interval to
10 minutes. - Paste the generated URL into
@heartbeat_url.
Step 5: Check Actor Coverage
PaperTrail records an originator_id when you pass :originator to each operation. If that's missing from version records, you lose the "who changed it" part of your audit trail:
# lib/my_app/paper_trail_actor_check.ex
defmodule MyApp.PaperTrailActorCheck do
alias MyApp.Repo
import Ecto.Query
def missing_actors_last_hour do
since = DateTime.utc_now() |> DateTime.add(-3600, :second)
from(v in PaperTrail.Version,
where: v.inserted_at >= ^since,
where: is_nil(v.originator_id),
select: count(v.id)
)
|> Repo.one()
end
end
Wire this into a separate heartbeat that pings Vigilmon only when missing_actors_last_hour() == 0. Any gap means your code paths aren't passing :originator.
Step 6: 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/paper-trail - The version write-rate heartbeat
- The actor coverage heartbeat (if created)
- The HTTP monitor on
For regulatory environments, create a Status Page in Vigilmon and add all PaperTrail monitors. Share the URL with your compliance team so they can self-serve on audit trail availability.
Key Metrics to Watch
| Metric | Vigilmon feature | Alert threshold | |---|---|---| | Versions table availability | HTTP monitor | Any 5xx or timeout | | Version write rate | Heartbeat | Missing for > 12 minutes | | Actor coverage | Heartbeat | Missing for > 1 heartbeat | | SSL certificate | Cert expiry monitor | Expires in < 14 days | | Response time | Response time chart | P95 > 500ms |
Conclusion
PaperTrail's version trail is only as reliable as the code paths that call it. Vigilmon's HTTP monitor catches database failures, the write-rate heartbeat catches code paths that silently stop versioning, and the actor check ensures compliance metadata is flowing. Together they give you early warning before an auditor asks "where are the logs?"
Get started free at vigilmon.online.