title: How to Monitor Guardian with Vigilmon published: true description: Add health checks, uptime monitoring, and alerts to Elixir/Phoenix apps using Guardian JWT authentication — detect token failures, expired secrets, and auth endpoint outages with Vigilmon. tags: elixir, phoenix, guardian, monitoring
Guardian is the standard JWT authentication library for Elixir and Phoenix. It handles token creation, verification, revocation, and refresh — the critical path for every authenticated request in your application.
When Guardian fails, users can't log in, API tokens stop working, and protected routes return unexpected errors. Because JWT validation is stateless and happens in-process, many Guardian failures are silent from the outside: the endpoint returns 401 or 403, but your uptime monitor only sees HTTP 200 on the root path.
This tutorial adds production observability to a Guardian-powered app with Vigilmon:
- A health endpoint that validates Guardian's token pipeline is functional
- HTTP uptime monitoring for authentication endpoints
- Heartbeat monitoring for token rotation and revocation jobs
- Slack alerts when auth fails
Why Monitor Guardian?
Guardian failures are easy to miss with standard HTTP monitoring because:
GET /may return 200 whilePOST /loginreturns 500 due to a bad secret config- Expired or rotated JWT secrets break token verification without crashing the app
- Token revocation backends (Redis, database) can fail silently
- Clock skew between services causes valid tokens to appear expired
Dedicated monitoring of your auth endpoints catches these failure modes before users do.
Step 1: Add a health check that validates Guardian
Add a health endpoint that performs a real token round-trip to verify Guardian's pipeline:
# 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 = run_checks()
status = if all_ok?(checks), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{status: status_label(status), checks: checks}))
|> halt()
end
def call(conn, _opts), do: conn
defp run_checks do
%{
database: check_database(),
guardian: check_guardian(),
memory: check_memory()
}
end
defp check_guardian do
# Mint a probe token and immediately verify it — validates the full pipeline
claims = %{"sub" => "health-check-probe", "typ" => "access"}
with {:ok, token, _claims} <- MyApp.Guardian.encode_and_sign(%{id: 0}, claims),
{:ok, _claims} <- MyApp.Guardian.decode_and_verify(token) do
:ok
else
{:error, reason} ->
require Logger
Logger.error("Guardian health check failed: #{inspect(reason)}")
:error
end
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
{:error, _} -> :error
end
end
defp check_memory do
case :memsup.get_system_memory_data() do
[] -> :ok
data ->
total = Keyword.get(data, :total_memory, 1)
free = Keyword.get(data, :free_memory, total)
if (total - free) / total * 100 < 90, do: :ok, else: :error
end
end
defp all_ok?(checks), do: Enum.all?(checks, fn {_, v} -> v == :ok end)
defp status_label(200), do: "ok"
defp status_label(_), do: "degraded"
end
Plug it in before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
mix phx.server
curl http://localhost:4000/health
# {"status":"ok","checks":{"database":"ok","guardian":"ok","memory":"ok"}}
If Guardian's secret key is missing or misconfigured, the health check returns 503 immediately.
Step 2: Monitor your authentication endpoints
Standard uptime monitoring checks that the server responds. For auth, you also want to monitor that the login and token endpoints are reachable and responding correctly.
Add a lightweight auth probe endpoint:
# lib/my_app_web/controllers/auth_probe_controller.ex
defmodule MyAppWeb.AuthProbeController do
use MyAppWeb, :controller
def ping(conn, _params) do
# Just confirms the auth routes are accessible and Guardian middleware loads
json(conn, %{auth: "ok", timestamp: System.system_time(:second)})
end
end
# lib/my_app_web/router.ex
scope "/auth", MyAppWeb do
pipe_through :api
get "/ping", AuthProbeController, :ping
post "/login", SessionController, :create
post "/refresh", SessionController, :refresh
end
Step 3: Set up HTTP monitoring in Vigilmon
Point Vigilmon at your health and auth endpoints:
Health monitor:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute (paid) or 5 minutes (free)
- Under Keyword Check, add
"guardian":"ok"to verify the JWT pipeline specifically - Save
Auth probe monitor:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/auth/ping - Add keyword check:
"auth":"ok" - Save
Now if your Guardian secret rotates incorrectly or the auth middleware fails to load, Vigilmon alerts via the keyword check even if the HTTP response is 200.
Step 4: Heartbeat monitoring for token rotation and revocation jobs
If you use Guardian with a database or Redis backend for token revocation (to support logout and token blacklisting), the revocation job can fail silently. Use a heartbeat monitor.
# lib/my_app/token_cleaner.ex
defmodule MyApp.TokenCleaner do
use GenServer
require Logger
@interval :timer.hours(1)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_clean()
{:ok, state}
end
@impl true
def handle_info(:clean, state) do
case clean_expired_tokens() do
{:ok, count} ->
Logger.info("Cleaned #{count} expired tokens")
ping_heartbeat()
{:error, reason} ->
Logger.error("Token cleanup failed: #{inspect(reason)}")
end
schedule_clean()
{:noreply, state}
end
defp schedule_clean, do: Process.send_after(self(), :clean, @interval)
defp clean_expired_tokens do
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
{count, _} = MyApp.Repo.delete_all(
from t in MyApp.GuardianToken,
where: t.inserted_at < ^cutoff
)
{:ok, count}
end
defp ping_heartbeat do
url = System.get_env("VIGILMON_TOKEN_CLEANER_HEARTBEAT_URL")
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, _} -> :ok
{:error, reason} -> Logger.warning("Heartbeat ping failed: #{inspect(reason)}")
end
end
end
end
In Vigilmon:
- Click New Monitor → Heartbeat
- Set expected interval to 70 minutes (buffer above the 1-hour clean cycle)
- Copy the ping URL → set as
VIGILMON_TOKEN_CLEANER_HEARTBEAT_URL
If the token table grows unbounded (cleanup failing), you'll know within the heartbeat window.
Step 5: Detect clock skew with JWT expiry checks
Clock skew between your app server and clients (or between microservices) causes valid tokens to appear expired. Add a check to your health endpoint that measures system time accuracy:
defp check_system_time do
# Basic sanity: system time is within reasonable range
now = System.system_time(:second)
# 2026-01-01 epoch, adjust as needed
expected_min = 1_751_328_000
if now > expected_min, do: :ok, else: :error
end
For multi-service architectures, also monitor that all services use NTP. Add an NTP check to each service's health endpoint and monitor them all from Vigilmon.
Step 6: Alerts via Slack
In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack incoming webhook URL.
Enable the Slack channel on your health, auth probe, and heartbeat monitors:
🔴 DOWN: yourdomain.com/health
Keyword check failed: "guardian":"ok" not found in response
Detected from: EU-West, US-East
3 minutes ago
This alert fires when Guardian fails its token round-trip — typically caused by a rotated or misconfigured JWT secret.
Step 7: Status page
- Go to Status Pages → New Status Page in Vigilmon
- Add your health monitor, auth probe monitor, and token cleaner heartbeat
- Share the URL with your team so they can check auth status first when users report login issues
What you've built
| What | How |
|------|-----|
| Guardian JWT round-trip check | encode_and_sign + decode_and_verify in health plug |
| Auth endpoint monitoring | Vigilmon HTTP + keyword check on /auth/ping |
| HTTP uptime monitoring | Vigilmon HTTP monitor → /health |
| Token revocation heartbeat | Heartbeat ping after each successful cleanup cycle |
| Slack downtime alerts | Vigilmon Slack notification channel |
| Status page | Vigilmon public status page |
Guardian secures your application. Vigilmon keeps external eyes on whether authentication is actually working.
Next steps
- Add separate monitors for each auth route:
/auth/login,/auth/refresh,/auth/logout - Use Vigilmon's response time history to detect when token verification slows down (often a sign of a saturated revocation store)
- Add a heartbeat for token refresh jobs if you rotate access tokens on a schedule
- Monitor your JWT secret rotation process — failed rotations are a common source of auth outages
Get started free at vigilmon.online.