tutorial

How to Monitor Ex_Audit with Vigilmon

Ex_Audit automatically records create, update, and delete operations on your Ecto schemas — but silent audit failures are hard to detect without external monitoring. Here's how to keep Ex_Audit healthy with Vigilmon.

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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your URL: https://yourapp.com/health/ex-audit.
  4. Set Check interval to 2 minutes.
  5. Set Expected HTTP status to 200.
  6. Under Advanced, add a Response body contains check: "status":"ok".
  7. 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:

  1. Click Add MonitorCron / Heartbeat.
  2. Set expected interval to 5 minutes.
  3. 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:

  1. Go to Alert ChannelsAdd Channel.
  2. Configure Slack, email, or PagerDuty.
  3. Set Notify after to 2 consecutive failures.
  4. Assign to:
    • The HTTP monitor on /health/ex-audit
    • The audit write-rate heartbeat
    • The metadata coverage heartbeat (if created)

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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →