tutorial

How to Monitor Surface UI with Vigilmon

Set up uptime monitoring, health checks, heartbeat monitors, and instant alerts for your Surface UI component library application with Vigilmon.

Surface UI is a server-side rendering component library for Phoenix LiveView that brings a Vue-like component syntax to the Elixir ecosystem. You declare components with typed props, slots, and event handlers — Surface's compiler transforms them into optimized LiveView code at compile time. The result is a structured, component-driven architecture that scales well as your UI grows. But Surface's compile-time magic means a bad component definition or a LiveView process crash can quietly break a section of your UI without affecting HTTP health probes. Vigilmon gives you the external monitoring layer to catch those failures: HTTP uptime checks, component endpoint validation, heartbeat monitors for data-feeding GenServers, and instant alerts to email or Slack.

What You'll Build

  • A /health endpoint that validates HTTP, database, and PubSub (the backing layer for Surface LiveComponent state)
  • Vigilmon HTTP monitors for your Surface-rendered pages
  • A heartbeat monitor for background workers that feed data to Surface components
  • Alert channels (email and Slack)
  • An uptime badge embeddable in your Surface layout

Prerequisites

  • A Phoenix LiveView + Surface UI application (Surface 0.11+)
  • A free Vigilmon account

Step 1: Add a Health Check Plug

Surface components render inside LiveView processes. Your health check should validate the Phoenix endpoint, Ecto, and PubSub — the full dependency chain for a live Surface component.

# 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, if(v == :ok, do: "ok", else: "error")}
      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
    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 before the router:

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

  plug MyAppWeb.Plugs.HealthCheck

  plug MyAppWeb.Router
end

Test it:

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

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: Surface-Rendered Page

Surface components render their HTML through LiveView over a WebSocket upgrade. An HTTP monitor on your main route confirms that the initial dead render (the HTML Plug sends before WebSocket connects) succeeds:

| Field | Value | |---|---| | URL | https://yourapp.com/ | | Method | GET | | Expected status | 200 | | Expected body | A stable string from your layout (e.g. your app name or a component's static outer tag) | | Check interval | 1 minute |

Two monitors, two failure domains. The root check catches Cowboy/Plug failures; the health check catches the data-layer failures that would cause Surface components to mount in a broken state.


Step 3: Heartbeat Monitor for Component Data Feeds

Surface LiveComponents that display live data (dashboards, feeds, tables) typically subscribe to PubSub topics that a GenServer populates. If that GenServer stops running — but its supervisor doesn't restart it within the expected window — components freeze without surfacing any HTTP error.

# lib/my_app/dashboard_feed.ex
defmodule MyApp.DashboardFeed do
  use GenServer

  require Logger

  @interval :timer.seconds(60)

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

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

  @impl true
  def handle_info(:broadcast, state) do
    case fetch_metrics() do
      {:ok, metrics} ->
        Phoenix.PubSub.broadcast(MyApp.PubSub, "dashboard:metrics", {:metrics, metrics})
        ping_heartbeat()

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

    schedule_broadcast()
    {:noreply, state}
  end

  defp schedule_broadcast, do: Process.send_after(self(), :broadcast, @interval)

  defp fetch_metrics do
    # Replace with your actual data fetching
    {:ok, %{requests_per_second: 42, error_rate: 0.1}}
  end

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

Register the server in your supervision tree:

# lib/my_app/application.ex
def start(_type, _args) do
  children = [
    MyApp.Repo,
    MyAppWeb.Endpoint,
    {Phoenix.PubSub, name: MyApp.PubSub},
    MyApp.DashboardFeed   # ← add here
  ]

  Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end

Add config:

# config/runtime.exs
config :my_app, :vigilmon,
  dashboard_heartbeat_url: System.get_env("VIGILMON_DASHBOARD_HEARTBEAT_URL")

In Vigilmon:

  1. Monitors → New Heartbeat Monitor
  2. Expected interval: 3 minutes (3× the 1-minute broadcast cycle)
  3. Copy the ping URL and set VIGILMON_DASHBOARD_HEARTBEAT_URL in your environment

Step 4: Embed an Uptime Badge in Your Surface Layout

Add a live uptime indicator to your Surface layout using a lightweight component:

# lib/my_app_web/components/uptime_badge.ex
defmodule MyAppWeb.Components.UptimeBadge do
  use Surface.Component

  prop badge_url, :string, required: true
  prop status_url, :string, required: true
  prop label, :string, default: "Uptime"

  def render(assigns) do
    ~F"""
    <a href={@status_url} target="_blank" rel="noopener noreferrer" aria-label="Service uptime status">
      <img
        src={@badge_url}
        alt={@label}
        width="120"
        height="20"
        loading="lazy"
      />
    </a>
    """
  end
end

Use it in your root layout:

# lib/my_app_web/components/layouts/root.html.heex (or .sface)
<footer>
  <MyAppWeb.Components.UptimeBadge
    badge_url="https://vigilmon.online/api/badge/your-monitor-id.svg"
    status_url="https://vigilmon.online/status/your-monitor-slug"
  />
</footer>

Because Surface renders server-side and LiveView persists the layout across navigations, the badge loads once and stays visible throughout the user's session.


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>"
}

Step 6: Verify Your Setup

# 1. Health endpoint returns 200 with all checks green
curl -s https://yourapp.com/health | jq .

# 2. Simulate a PubSub failure (stop the PubSub process)
# Expected: health returns 503 with "pubsub": "error"
# Expected: Vigilmon alerts within 2 minutes (catches frozen Surface components)

# 3. Stop DashboardFeed and wait for the heartbeat window to expire
# Expected: Vigilmon heartbeat alert fires before users notice stale data

# 4. Vigilmon → "Test Alert" → verify email and Slack delivery

Summary

| Monitor | What It Catches | |---|---| | HTTP health endpoint | Database failures, PubSub outages, bad deploys | | Surface-rendered page root | Plug/Cowboy failures, broken dead-render | | GenServer heartbeat | Frozen dashboard feeds, stalled PubSub broadcasts |


Next Steps

  • Add a heartbeat for every GenServer that feeds a critical Surface component
  • Use Vigilmon's response time graphs to correlate Surface component mount latency with data-fetch times
  • Monitor the LiveView WebSocket upgrade path (/live/websocket) to catch endpoint misconfigurations
  • Add multi-region monitors if you deploy across multiple Fly.io regions

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 →