tutorial

How to Monitor Phoenix LiveView with Vigilmon

Set up uptime monitoring, WebSocket health checks, heartbeat monitors, and instant alerts for your Phoenix LiveView application with Vigilmon.

Phoenix LiveView lets you build rich, real-time user interfaces without writing JavaScript — server-rendered HTML flows over a persistent WebSocket connection, delivering the responsiveness of a single-page app with the simplicity of server-side code. But WebSocket-heavy architectures introduce failure modes that traditional HTTP monitoring misses: a LiveView process can crash silently, a reconnect storm after a deploy can overwhelm the node, or an upstream connection pool can exhaust while the HTTP layer stays green. Vigilmon gives you the external observability layer your LiveView stack needs: HTTP uptime checks, WebSocket endpoint monitoring, heartbeat monitors for PubSub subscribers and background workers, and instant alerts to email or Slack.

What You'll Build

  • A /health endpoint covering HTTP, database, and PubSub connectivity
  • A Vigilmon HTTP monitor for the LiveView WebSocket upgrade path
  • A heartbeat monitor for GenServer-based LiveView data feeds
  • Alert channels (email and Slack)
  • A public status page for your users

Prerequisites

  • A Phoenix LiveView app (Phoenix 1.7+)
  • A free Vigilmon account

Step 1: Add a Health Check Plug

LiveView's WebSocket transport sits on top of your Phoenix endpoint. A health check that validates HTTP, Ecto, and PubSub gives Vigilmon the full picture.

# 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(),
      pubsub: check_pubsub()
    }

    degraded = Enum.any?(checks, fn {_, v} -> v != :ok end)
    status = if degraded, do: 503, else: 200

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{
      status: if(degraded, do: "degraded", else: "ok"),
      checks: Map.new(checks, fn {k, v} -> {k, Atom.to_string(v)} end)
    }))
    |> 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, _} -> :error
    end
  end

  defp check_pubsub do
    topic = "health:check:#{System.unique_integer([:positive])}"
    Phoenix.PubSub.subscribe(MyApp.PubSub, topic)
    Phoenix.PubSub.broadcast(MyApp.PubSub, topic, :ping)

    result =
      receive do
        :ping -> :ok
      after
        1_000 -> :error
      end

    Phoenix.PubSub.unsubscribe(MyApp.PubSub, topic)
    result
  end
end

Register the plug in your endpoint before the router:

# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  plug MyAppWeb.Plugs.HealthCheck  # before authentication middleware

  plug MyAppWeb.Router
end

Test it:

curl -s http://localhost:4000/health | jq .
# {
#   "status": "ok",
#   "checks": { "database": "ok", "pubsub": "ok" }
# }

A PubSub failure returns 503 — surfacing the exact dependency that would cause LiveView channels to stop broadcasting.


Step 2: Configure Vigilmon HTTP Monitors

Log in to VigilmonMonitors → New Monitor.

Monitor 1: Health Endpoint

| Field | Value | |---|---| | URL | https://yourapp.com/health | | Method | GET | | Expected status | 200 | | Expected body | "status":"ok" | | Check interval | 1 minute |

Monitor 2: LiveView WebSocket Upgrade Path

LiveView negotiates a WebSocket connection at /live/websocket. An HTTP GET to this endpoint returns a 101 Switching Protocols or a redirect — but Vigilmon can confirm the path is reachable (not returning 404 or 500) which catches mis-configured Nginx upstreams and broken deploy artifacts.

| Field | Value | |---|---| | URL | https://yourapp.com/live/websocket | | Method | GET | | Expected status | 200–404 (any non-5xx) | | Check interval | 1 minute |


Step 3: Heartbeat Monitor for GenServer Data Feeds

LiveView mounts frequently read from a GenServer or Phoenix.Tracker that polls external data. If that process crashes and the supervisor gives up retrying, your LiveViews stall silently — the process tree is healthy but data has stopped flowing.

# lib/my_app/feed_server.ex
defmodule MyApp.FeedServer do
  use GenServer

  require Logger

  @poll_interval :timer.seconds(30)

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

  @impl true
  def init(state) do
    schedule_poll()
    {:ok, state}
  end

  @impl true
  def handle_info(:poll, state) do
    case fetch_and_broadcast() do
      :ok ->
        ping_heartbeat()

      {:error, reason} ->
        Logger.error("FeedServer poll failed: #{inspect(reason)}")
        # No heartbeat ping — Vigilmon alerts after the window
    end

    schedule_poll()
    {:noreply, state}
  end

  defp schedule_poll, do: Process.send_after(self(), :poll, @poll_interval)

  defp fetch_and_broadcast do
    # Replace with your actual data-fetching logic
    Phoenix.PubSub.broadcast(MyApp.PubSub, "feed:updates", {:data, %{}})
    :ok
  end

  defp ping_heartbeat do
    url = Application.get_env(:my_app, :vigilmon)[:feed_heartbeat_url]
    if url, do: Req.get(url, receive_timeout: 5_000)
  end
end

Add config:

# config/runtime.exs
config :my_app, :vigilmon,
  feed_heartbeat_url: System.get_env("VIGILMON_FEED_HEARTBEAT_URL")

In Vigilmon:

  1. Monitors → New Heartbeat Monitor
  2. Expected interval: 2 minutes (4× the 30-second poll)
  3. Copy the ping URL and set VIGILMON_FEED_HEARTBEAT_URL in your environment

Step 4: Monitor LiveView Channel Recovery After Deploys

Rolling deploys disconnect existing LiveView clients. Reconnection storms can spike memory and cause the node to become unresponsive before the load balancer health check catches it.

Add a post-deploy check in your CI/CD pipeline:

#!/usr/bin/env bash
# deploy_verify.sh — run after every production deploy

set -euo pipefail

HEALTH_URL="https://yourapp.com/health"
MAX_RETRIES=12
SLEEP=10

for i in $(seq 1 $MAX_RETRIES); do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL")
  if [ "$STATUS" = "200" ]; then
    echo "Health check passed (attempt $i)"
    exit 0
  fi
  echo "Attempt $i: got $STATUS, retrying in ${SLEEP}s..."
  sleep $SLEEP
done

echo "Health check failed after $MAX_RETRIES attempts"
exit 1

Pair this with Vigilmon's 1-minute polling: if the reconnection surge degrades your health endpoint, Vigilmon alerts independently of your deploy pipeline.


Step 5: Alert Channels

In Vigilmon → Alert Channels:

Email

  1. Alert Channels → Email → add your on-call address
  2. Attach to all monitors

Slack

  1. Create a Slack Incoming Webhook for #alerts
  2. Alert Channels → Webhook → paste the URL
  3. Payload template:
{
  "text": "🔴 *{{ monitor.name }}* is DOWN\n{{ monitor.url }}\nStatus: {{ event.status }}\n<https://vigilmon.online|Open Vigilmon>"
}

Vigilmon sends a matching ✅ RECOVERED notification when the monitor comes back up.


Step 6: Public Status Page

  1. Vigilmon → Status Pages → New Status Page
  2. Add your health endpoint monitor and the WebSocket path monitor
  3. Copy the public URL and share it in your README or error pages
![Uptime](https://vigilmon.online/api/badge/your-monitor-id.svg)

Summary

| Monitor | What It Catches | |---|---| | HTTP health endpoint | Database failures, PubSub outages, broken deploys | | LiveView WebSocket path | Nginx misconfiguration, endpoint process crashes | | GenServer heartbeat | Silent feed failures, stalled PubSub broadcasts |


Next Steps

  • Add :memsup memory data to your health response to catch reconnection-storm OOM conditions early
  • Monitor each Fly.io region separately if you run a distributed cluster
  • Use Vigilmon's response time graphs to correlate LiveView latency spikes with database query slowdowns
  • Add a heartbeat for every Oban queue that drives LiveView updates

Ready to get started? Sign up for Vigilmon free — have your first monitor running in under 5 minutes.

Monitor your app with Vigilmon

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

Start free →