tutorial

How to Monitor Ash.Authentication with Vigilmon

Monitor Ash.Authentication flows — track login success rates, OAuth2 callback health, magic link delivery, token expiry, and API key validation with external uptime checks and heartbeat monitoring.

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

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set interval: 1 minute
  5. Add a keyword check: alert when "error" appears in the response body
  6. 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:

  1. Go to Monitors → New → Heartbeat
  2. Set grace period: 25 hours (daily job + 1 hour buffer)
  3. 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

  1. In Vigilmon, go to Status Pages → New
  2. Add: health endpoint, login route monitor, OAuth2 callback monitor, token cleanup heartbeat
  3. Name them: "Auth API", "Login Page", "OAuth2 Provider", "Token Maintenance"
  4. 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.

Monitor your app with Vigilmon

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

Start free →