tutorial

How to Monitor Phx.Gen.Auth with Vigilmon

Add uptime monitoring, authentication flow health checks, and alerts to your Phx.Gen.Auth-powered Phoenix application — so registration failures, session store outages, and login regressions don't go undetected.

How to Monitor Phx.Gen.Auth with Vigilmon

Phx.Gen.Auth generates a complete authentication system for Phoenix applications: user registration, login, password reset, email confirmation, and session management — all wired up with secure defaults. But auth is the most critical path in any web application. When the session store degrades, login starts failing silently. When the email delivery service hiccups, password reset tokens expire before users receive them.

External monitoring surfaces what Phx.Gen.Auth can't self-report. This tutorial covers:

  • A health endpoint that validates auth-layer dependencies
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring to detect when token-cleanup jobs stall
  • Slack alerts and a status page

Step 1: Generate and configure Phx.Gen.Auth

If you haven't already scaffolded authentication:

mix phx.gen.auth Accounts User users
mix ecto.migrate

Add the HTTP client for heartbeat pings:

# mix.exs
defp deps do
  [
    # ... generated auth deps (bcrypt_elixir, etc.)
    {:req, "~> 0.4"}
  ]
end

Step 2: Build a health endpoint that validates auth dependencies

Auth depends on three things: the database (for user records and tokens), the session store (ETS or Redis), and optionally email delivery. Check all three:

# 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 checks.status == "ok", do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(checks))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp run_checks do
    db = database_check()
    session = session_store_check()

    all_ok = db.status == "ok" and session.status == "ok"

    %{
      status: if(all_ok, do: "ok", else: "degraded"),
      database: db,
      session_store: session
    }
  end

  defp database_check do
    start = System.monotonic_time(:millisecond)

    case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", [], timeout: 3_000) do
      {:ok, _} ->
        %{status: "ok", latency_ms: System.monotonic_time(:millisecond) - start}

      {:error, reason} ->
        %{status: "error", reason: inspect(reason)}
    end
  end

  defp session_store_check do
    # Phx.Gen.Auth uses cookie sessions by default; validate the signing key is configured
    secret = Application.get_env(:my_app, MyAppWeb.Endpoint)[:secret_key_base]

    if is_binary(secret) and byte_size(secret) >= 64 do
      %{status: "ok", type: "cookie"}
    else
      %{status: "error", reason: "secret_key_base missing or too short"}
    end
  end
end

Register the plug before your router:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router

Test it:

mix phx.server
curl http://localhost:4000/health | jq .
# {
#   "status": "ok",
#   "database": {"status": "ok", "latency_ms": 2},
#   "session_store": {"status": "ok", "type": "cookie"}
# }

Step 3: Track auth metrics with telemetry

Phoenix emits telemetry for every request. Attach a handler that counts authentication events:

# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
  require Logger

  def setup do
    :telemetry.attach(
      "auth-request-logger",
      [:phoenix, :router_dispatch, :stop],
      &handle_dispatch/4,
      nil
    )
  end

  def handle_dispatch(_event, measurements, %{route: route, conn: conn}, _cfg) do
    duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)

    cond do
      route in ["/users/log_in", "/users/register"] and conn.status == 200 ->
        Logger.info("[Auth] successful request to #{route} in #{duration_ms}ms")

      route in ["/users/log_in", "/users/register"] and conn.status >= 400 ->
        Logger.warning("[Auth] failed request to #{route} — status=#{conn.status} #{duration_ms}ms")

      true ->
        :ok
    end
  end
end

Call MyApp.Telemetry.setup() in application.ex:

def start(_type, _args) do
  MyApp.Telemetry.setup()
  # ...
end

Auth failures now surface in your logs — and you can configure a Vigilmon keyword monitor on a log-shipping health endpoint to alert when failure rate spikes.


Step 4: Monitor the health endpoint with Vigilmon

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. Enable keyword check: body must contain "status":"ok"
  6. Save

Also add a monitor directly on your login page to catch routing regressions:

  1. Click New Monitor → HTTP
  2. Enter https://yourdomain.com/users/log_in
  3. Set check interval: 5 minutes
  4. Enable keyword check: body must contain Log in
  5. Save

If a bad deploy removes the /users/log_in route, Vigilmon catches it within five minutes.


Step 5: Heartbeat monitoring for token cleanup jobs

Phx.Gen.Auth generates a UserToken schema. Stale tokens accumulate unless you run periodic cleanup. Monitor that cleanup with a heartbeat:

# lib/my_app/workers/token_cleanup_worker.ex
defmodule MyApp.Workers.TokenCleanupWorker do
  use GenServer
  require Logger

  @interval :timer.hours(1)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  def init(state) do
    schedule()
    {:ok, state}
  end

  def handle_info(:run, state) do
    run_cleanup()
    schedule()
    {:noreply, state}
  end

  defp run_cleanup do
    # Delete expired session and reset tokens
    cutoff = DateTime.add(DateTime.utc_now(), -7, :day)

    {deleted, _} =
      MyApp.Repo.delete_all(
        from t in MyApp.Accounts.UserToken,
          where: t.inserted_at < ^cutoff and t.context != "session"
      )

    Logger.info("[TokenCleanup] deleted #{deleted} expired tokens")
    ping_vigilmon()
  rescue
    e ->
      Logger.error("[TokenCleanup] failed: #{Exception.message(e)}")
  end

  defp ping_vigilmon do
    url = Application.get_env(:my_app, :vigilmon)[:heartbeat_url]

    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, %{status: 200}} -> :ok
        error -> Logger.warning("Vigilmon heartbeat ping failed: #{inspect(error)}")
      end
    end
  end

  defp schedule, do: Process.send_after(self(), :run, @interval)
end

Add to your supervision tree:

children = [
  MyApp.Repo,
  MyAppWeb.Endpoint,
  MyApp.Workers.TokenCleanupWorker
]

In Vigilmon, create a Heartbeat Monitor:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 2 hours
  3. Copy the ping URL
  4. Set it as VIGILMON_HEARTBEAT_URL in your environment

If the cleanup job crashes or the database becomes unreachable, the heartbeat stops — Vigilmon alerts you within two missed intervals.


Step 6: Monitor the password reset flow

Password reset is the highest-stakes auth path: a broken reset flow locks users out permanently. Add a synthetic check:

# In your health check plug
defp password_reset_check do
  # Verify the reset endpoint is reachable and returns the form
  url = Application.get_env(:my_app, :base_url) <> "/users/reset_password"

  case Req.get(url, receive_timeout: 3_000) do
    {:ok, %{status: 200, body: body}} ->
      if String.contains?(body, "Forgot your password") do
        %{status: "ok"}
      else
        %{status: "error", reason: "reset page content mismatch"}
      end

    {:ok, %{status: status}} ->
      %{status: "error", reason: "unexpected HTTP #{status}"}

    {:error, reason} ->
      %{status: "error", reason: inspect(reason)}
  end
end

Add it to run_checks/0:

defp run_checks do
  db = database_check()
  session = session_store_check()
  reset = password_reset_check()

  all_ok = db.status == "ok" and session.status == "ok" and reset.status == "ok"

  %{
    status: if(all_ok, do: "ok", else: "degraded"),
    database: db,
    session_store: session,
    password_reset: reset
  }
end

Step 7: Slack alerts and status page

Slack alerts:

  1. In Vigilmon, go to Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. Enable the channel on your health monitor, login monitor, and heartbeat monitor

When the database goes down:

🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: US-East
2 minutes ago

When the login page is unreachable:

🔴 DOWN: yourdomain.com/users/log_in
Keyword "Log in" not found in response body
Detected: EU-West
5 minutes ago

Status page:

  1. Go to Status Pages → New Status Page
  2. Add your health monitor, login monitor, and heartbeat monitor
  3. Share the URL with your team

What you've built

| What | How | |------|-----| | Health endpoint | Plug checking DB, session store, and password reset path | | Keyword check | Vigilmon keyword match on "status":"ok" | | Uptime monitoring | Vigilmon HTTP monitor → /health | | Login page check | Vigilmon HTTP monitor → /users/log_in | | Token cleanup | Heartbeat GenServer + Vigilmon heartbeat monitor | | Auth telemetry | Phoenix telemetry handler logging auth request outcomes | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Phx.Gen.Auth makes your authentication solid. Vigilmon tells you when it cracks.


Next steps

  • Add a monitor on the /users/confirm path to catch email confirmation regressions
  • Alert on elevated 401/403 rates from your access logs to detect brute-force attempts early
  • Use Vigilmon's response time history to track login latency trends as your user table 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 →