How to Monitor Ash.Authentication with Vigilmon
Ash.Authentication is the batteries-included authentication extension for the Ash Framework. It wires up password auth, OAuth2 providers, magic links, and API token strategies directly into your Ash resources — no custom controller logic, no custom contexts. Authorization integrates through Ash policies, so access control lives in the same declarative layer as your data model.
When authentication breaks, every authenticated endpoint in your application goes with it. A failing OAuth2 callback returns a 500 to users who are mid-login. A broken magic link email pipeline means password-less users are locked out silently. An expired signing key invalidates all active sessions simultaneously. These failures are catastrophic and the window to detect them is narrow — you need external monitoring, not just internal error logs.
This tutorial covers:
- A health check endpoint that validates your auth strategies and signing key availability
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for magic link and token cleanup tasks
- Alerts for elevated auth failure rates and session issues
Why Monitor Ash.Authentication?
Authentication failures are high-impact and often silent until a user reports them:
| Failure mode | Symptom without monitoring | |---|---| | Signing key missing or rotated | All sessions invalid; mass logout | | OAuth2 provider unreachable | 500 on callback; users see generic error | | Magic link mailer down | Users silently can't log in | | Token cleanup job not running | Database grows; query performance degrades | | bcrypt cost too high for server | Login timeouts under load | | CSRF token mismatch after deploy | Login form submits fail with 403 |
Step 1: Health check endpoint
Add a plug that checks auth-layer dependencies — the signing key, database, and optionally a test OAuth2 provider connectivity:
# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = %{
database: check_database(),
signing_key: check_signing_key(),
token_store: check_token_store()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{
status: if(status == 200, do: "ok", else: "degraded"),
checks: checks
}))
|> halt()
end
def call(conn, _opts), do: conn
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp check_signing_key do
# Verify the JWT signing secret is set and non-empty
secret = Application.get_env(:my_app, :token_signing_secret)
if is_binary(secret) and byte_size(secret) >= 32, do: :ok, else: :error
end
defp check_token_store do
# Count recently issued tokens — if the table is queryable, auth storage is healthy
case Ecto.Adapters.SQL.query(
MyApp.Repo,
"SELECT COUNT(*) FROM user_tokens WHERE inserted_at > NOW() - INTERVAL '1 hour'",
[]
) do
{:ok, _} -> :ok
_ -> :error
end
end
end
Plug it before the router and any authentication plugs:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Step 2: Track auth strategy outcomes with telemetry
Add a GenServer that aggregates login success/failure rates so you can expose them in the health response:
# lib/my_app/auth_monitor.ex
defmodule MyApp.AuthMonitor do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, initial_state(), name: __MODULE__)
def record_success(strategy), do: GenServer.cast(__MODULE__, {:success, strategy})
def record_failure(strategy), do: GenServer.cast(__MODULE__, {:failure, strategy})
def stats, do: GenServer.call(__MODULE__, :stats)
defp initial_state, do: %{successes: %{}, failures: %{}}
@impl true
def init(state), do: {:ok, state}
@impl true
def handle_cast({:success, strategy}, state) do
{:noreply, update_in(state, [:successes, strategy], &((&1 || 0) + 1))}
end
def handle_cast({:failure, strategy}, state) do
{:noreply, update_in(state, [:failures, strategy], &((&1 || 0) + 1))}
end
@impl true
def handle_call(:stats, _from, state) do
total_s = Enum.sum(Map.values(state.successes))
total_f = Enum.sum(Map.values(state.failures))
failure_rate = if total_s + total_f > 0,
do: Float.round(total_f / (total_s + total_f) * 100, 1),
else: 0.0
{:reply, Map.merge(state, %{total_successes: total_s, total_failures: total_f, failure_rate_pct: failure_rate}), state}
end
end
Add to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.AuthMonitor,
]
Call record_success/1 and record_failure/1 from your Ash Authentication action result handler. For example, in a custom auth plug:
case AshAuthentication.authenticate(resource, params) do
{:ok, user} ->
MyApp.AuthMonitor.record_success(:password)
{:ok, user}
{:error, reason} ->
MyApp.AuthMonitor.record_failure(:password)
{:error, reason}
end
Update the health plug to include the failure rate:
defp check_auth_failure_rate do
stats = MyApp.AuthMonitor.stats()
if stats.failure_rate_pct < 20.0, do: :ok, else: :degraded
end
Step 3: Set up HTTP monitoring in Vigilmon
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set interval: 1 minute
- Add a keyword check: alert when
"error"appears in the response body - Set threshold: 2 consecutive failures
Add a second monitor for your login page:
- URL:
https://yourdomain.com/sign-in(or your auth route) - Expected status:
200 - Keyword: check for a string that appears on your login form (e.g.
"Sign in")
This catches cases where the login route itself crashes even when the health endpoint is still reachable.
Step 4: Monitor OAuth2 callback availability
OAuth2 callbacks are often the first thing to break after a configuration change. Add a dedicated monitor:
- URL:
https://yourdomain.com/auth/github/callback(replace with your provider) - Expected status: 302 (redirect after successful OAuth exchange) or 400 (missing params — expected for an unauthenticated probe)
- Note: Vigilmon can check for specific status codes, so configure it to accept 400 as "healthy" since a bare GET without an OAuth code is expected to fail gracefully rather than 500
If this endpoint starts returning 500, your OAuth2 configuration (client secret, redirect URI, provider availability) has broken.
Step 5: Heartbeat monitoring for token cleanup jobs
Ash.Authentication issues tokens stored in your database. Without a cleanup job, the user_tokens table grows indefinitely. Monitor your cleanup job with a heartbeat:
# lib/my_app/workers/token_cleanup_worker.ex
defmodule MyApp.Workers.TokenCleanupWorker do
use Oban.Worker, queue: :maintenance, max_attempts: 3
@heartbeat_url System.get_env("VIGILMON_TOKEN_CLEANUP_HEARTBEAT_URL")
@impl Oban.Worker
def perform(%Oban.Job{}) do
cutoff = DateTime.add(DateTime.utc_now(), -7 * 24 * 3600, :second)
{deleted, _} = MyApp.Repo.delete_all(
from t in MyApp.UserToken,
where: t.inserted_at < ^cutoff
)
require Logger
Logger.info("TokenCleanup: deleted #{deleted} expired tokens")
ping_heartbeat()
:ok
end
defp ping_heartbeat do
if @heartbeat_url do
Task.start(fn ->
:httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 5000}], [])
end)
end
end
end
Schedule it with Oban:
# config/config.exs
config :my_app, Oban,
queues: [default: 10, maintenance: 2],
plugins: [
{Oban.Plugins.Cron,
crontab: [
{"0 3 * * *", MyApp.Workers.TokenCleanupWorker} # daily at 3 AM
]}
]
Create the heartbeat in Vigilmon:
- Go to Monitors → New → Heartbeat
- Set grace period: 25 hours (daily job + 1 hour buffer)
- Copy the ping URL into
VIGILMON_TOKEN_CLEANUP_HEARTBEAT_URL
Step 6: Key metrics to alert on
| Metric | Alert threshold | Why |
|---|---|---|
| /health HTTP status | Non-200 for 2 checks | Auth layer unavailable |
| signing_key check | "error" in body | Sessions being invalidated |
| Login route status | Non-200 | Login page broken |
| OAuth2 callback status | 500 (not 302 or 400) | OAuth config broken |
| failure_rate_pct | > 20% | Elevated auth failures |
| Token cleanup heartbeat | Grace period exceeded | Token table growing unbounded |
Step 7: Status page
- In Vigilmon, go to Status Pages → New
- Add: health endpoint, login route monitor, OAuth2 callback monitor, token cleanup heartbeat
- Name them: "Auth API", "Login Page", "OAuth2 Provider", "Token Maintenance"
- Share the URL with your support team so they can direct users to it
Conclusion
Ash.Authentication consolidates multiple auth strategies into a single coherent layer — and that consolidation means a single failure can break all of them at once. With Vigilmon you get:
- HTTP checks that catch crashed auth routes and missing signing keys before users are locked out
- OAuth2 callback monitors that detect provider configuration failures immediately
- Heartbeat monitors that ensure your token cleanup jobs are running and your database stays healthy
Start monitoring your login route and health endpoint today, and add strategy-specific checks as your auth configuration grows.
Get started free at vigilmon.online.