How to Monitor OpenApiSpex with Vigilmon
OpenApiSpex brings OpenAPI 3.0 to Phoenix: define your schemas with a DSL, get automatic Swagger UI, and validate every request and response at compile time. It's the idiomatic way to build documented, type-safe Phoenix APIs.
When your API is contract-driven with OpenApiSpex, external monitoring becomes even more important. A broken health check or spec endpoint means SDK generators, developer portals, and API consumers all fail silently. You need eyes on /api-spec, Swagger UI, and every critical endpoint — not just your homepage.
This tutorial wires production observability into a Phoenix + OpenApiSpex API:
- A standards-compliant health check endpoint (complete with its own OpenApiSpex schema)
- HTTP uptime monitoring with Vigilmon
- Monitoring for the spec endpoint and Swagger UI
- Heartbeat monitoring for background workers
- Slack alerts and a status page
Why Monitor OpenApiSpex Applications?
OpenApiSpex-powered APIs have unique failure modes beyond simple uptime:
- The spec endpoint (
/api-specor/api/openapi) goes down and SDK generators start failing - Swagger UI becomes unreachable and developers lose their interactive docs
- Request validation rejects valid payloads because a schema was published without a corresponding migration
- Response validation fails silently because the controller returns a shape that doesn't match the declared schema
Vigilmon catches the first two via HTTP monitoring. The last two require health checks that exercise your schema validation layer, which this tutorial covers.
Step 1: Define a Health Check Schema with OpenApiSpex
Because you're already using OpenApiSpex, define your health endpoint response as a proper schema. This documents the contract and lets you validate it in tests.
# lib/my_app_web/schemas/health.ex
defmodule MyAppWeb.Schemas.Health do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "HealthResponse",
type: :object,
properties: %{
status: %Schema{type: :string, enum: ["ok", "degraded"]},
checks: %Schema{
type: :object,
properties: %{
database: %Schema{type: :string, enum: ["ok", "error"]},
cache: %Schema{type: :string, enum: ["ok", "error"]},
spec: %Schema{type: :string, enum: ["ok", "error"]}
}
},
version: %Schema{type: :string}
},
required: [:status, :checks]
})
end
Step 2: Add the Health Check Controller
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
use OpenApiSpex.ControllerSpecs
alias MyAppWeb.Schemas.Health
operation :show,
summary: "Health check",
description: "Returns the current health status of the API and its dependencies.",
responses: [
ok: {"Health response", "application/json", Health},
service_unavailable: {"Degraded response", "application/json", Health}
]
def show(conn, _params) do
checks = %{
database: check_database(),
cache: check_cache(),
spec: check_spec()
}
status = if Enum.all?(checks, fn {_, v} -> v == "ok" end), do: :ok, else: :service_unavailable
conn
|> put_status(status)
|> json(%{
status: if(status == :ok, do: "ok", else: "degraded"),
checks: checks,
version: Application.spec(:my_app, :vsn) |> to_string()
})
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> "ok"
_ -> "error"
end
end
defp check_cache do
case Redix.command(:redix, ["PING"]) do
{:ok, "PONG"} -> "ok"
_ -> "error"
end
end
defp check_spec do
# Verify the spec can be generated (compilation-time validation caught earlier,
# this catches runtime resolution issues)
case OpenApiSpex.Plug.PutApiSpec.get_spec(MyAppWeb.ApiSpec) do
{:ok, _} -> "ok"
_ -> "error"
end
rescue
_ -> "error"
end
end
Wire it into your router:
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug OpenApiSpex.Plug.PutApiSpec, module: MyAppWeb.ApiSpec
end
# Health check — no authentication middleware
scope "/", MyAppWeb do
get "/health", HealthController, :show
end
scope "/api", MyAppWeb do
pipe_through :api
# ... your regular routes
end
end
Test it:
mix phx.server
curl http://localhost:4000/health
# {"status":"ok","checks":{"database":"ok","cache":"ok","spec":"ok"},"version":"0.1.0"}
Step 3: Monitor the Spec Endpoint
The /api-spec endpoint is often overlooked in monitoring but critical for API consumers. Add a separate Vigilmon monitor for it:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/api-spec(or wherever you serve the spec) - Enable a keyword check: body must contain
"openapi"to confirm a valid JSON spec is returned - Set check interval to 1 minute
- Save
Add a second monitor for Swagger UI:
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/swaggerui - Enable a keyword check: body must contain
swagger-ui(from the HTML) - Save
Step 4: Set Up HTTP Monitoring for the Health Endpoint
Add the main health monitor:
- In Vigilmon, click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set interval to 1 minute
- Optionally add a keyword check for
"ok"to catch degraded responses that still return 200
Step 5: Heartbeat Monitoring for Background Workers
If your API has background workers (sending webhooks, syncing data, processing jobs), add heartbeat monitoring for each one.
# lib/my_app/workers/webhook_dispatcher.ex
defmodule MyApp.Workers.WebhookDispatcher do
use GenServer
require Logger
@interval :timer.seconds(30)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_tick()
{:ok, state}
end
@impl true
def handle_info(:tick, state) do
case dispatch_pending_webhooks() do
{:ok, count} ->
Logger.info("Dispatched #{count} webhooks")
ping_heartbeat()
{:error, reason} ->
Logger.error("Webhook dispatch failed: #{inspect(reason)}")
end
schedule_tick()
{:noreply, state}
end
defp schedule_tick, do: Process.send_after(self(), :tick, @interval)
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:webhook_heartbeat_url]
if url, do: Req.get(url, receive_timeout: 5_000)
end
defp dispatch_pending_webhooks do
# your dispatch logic
{:ok, 0}
end
end
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval (e.g. 1 minute for a 30-second worker, with some slack)
- Copy the ping URL and set it as
VIGILMON_WEBHOOK_HEARTBEAT_URL
Step 6: Alerts via Slack
In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack webhook URL.
Assign the Slack channel to all three monitors (health, spec, Swagger UI). On downtime:
🔴 DOWN: yourdomain.com/api-spec
Status: 503 Service Unavailable
Detected from: EU-West, US-East
2 minutes ago
Having separate monitors means you know exactly which endpoint failed — the whole API, just the spec, or just the docs UI.
Step 7: Status Page and Badge
- Go to Status Pages → New Status Page in Vigilmon
- Add all three monitors (health, spec, Swagger UI)
- Copy the public URL and add it to your API documentation

Key Metrics for OpenApiSpex APIs
| Monitor | What it catches |
|---------|----------------|
| /health HTTP check | DB, cache, and spec generation health |
| /api-spec HTTP + keyword check | Spec endpoint down or returning invalid JSON |
| /swaggerui HTTP + keyword check | Developer docs unreachable |
| Webhook heartbeat | Background dispatch worker stalled |
| Response time trend | Slow schema validation causing latency |
What You've Built
| What | How | |------|-----| | Typed health schema | OpenApiSpex schema with spec validation check | | Health endpoint | Phoenix controller, bypasses auth middleware | | HTTP uptime monitoring | Vigilmon monitors for health, spec, and Swagger UI | | Keyword checks | Verify spec and UI content, not just HTTP status | | Heartbeat monitoring | Webhook dispatcher GenServer | | Slack alerts | Per-monitor notifications | | Public status page | All monitors in one place |
OpenApiSpex keeps your API contract honest at compile time. Vigilmon keeps the deployed endpoints reachable at runtime.
Next Steps
- Add a monitor for each versioned spec endpoint if you maintain multiple API versions
- Use Vigilmon's response time tracking to detect spec generation regressions after schema additions
- Add a POST monitor that validates an example request payload goes through successfully end-to-end
Get started free at vigilmon.online.