tutorial

How to Monitor Guardian with Vigilmon

--- title: How to Monitor Guardian with Vigilmon published: true description: Add health checks, uptime monitoring, and alerts to Elixir/Phoenix apps using ...


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 while POST /login returns 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:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute (paid) or 5 minutes (free)
  5. Under Keyword Check, add "guardian":"ok" to verify the JWT pipeline specifically
  6. Save

Auth probe monitor:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/auth/ping
  3. Add keyword check: "auth":"ok"
  4. 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:

  1. Click New Monitor → Heartbeat
  2. Set expected interval to 70 minutes (buffer above the 1-hour clean cycle)
  3. 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

  1. Go to Status Pages → New Status Page in Vigilmon
  2. Add your health monitor, auth probe monitor, and token cleaner heartbeat
  3. 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.

Monitor your app with Vigilmon

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

Start free →