Ash Framework is a declarative, resource-based application framework for Elixir that lets you model your domain once and derive APIs, authorization policies, and persistence layers from that single source of truth. Whether you're serving a JSON:API, a GraphQL schema, or a LiveView interface, all of it flows through Ash resources — which means a single resource misconfiguration, a broken Ecto adapter, or an exhausted connection pool can silently cascade across every interface your app exposes. Vigilmon gives you the external observability layer to catch those failures before your users do: HTTP uptime checks, heartbeat monitors for Ash Reactor pipelines, and instant alerts to email or Slack.
What You'll Build
- A
/healthendpoint that validates your Ash data layer and API surface - Vigilmon HTTP monitors for your Ash JSON:API and GraphQL endpoints
- A heartbeat monitor for Ash Reactor or scheduled Ash actions
- Alert channels (email and Slack)
Prerequisites
- An Ash Framework application (Ash 3.x)
- A free Vigilmon account
Step 1: Add a Health Check Endpoint
Ash apps typically expose APIs through AshJsonApi or AshGraphql. Your health check should validate the data layer that backs those APIs.
# 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(),
ash_resources: check_ash_resources()
}
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_ash_resources do
# Perform a lightweight read against a representative Ash resource
# to verify the data layer is responsive
case Ash.read(MyApp.Accounts.User, action: :list, page: [limit: 1]) do
{:ok, _} -> :ok
{:error, _} -> :error
end
rescue
_ -> :error
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
# AshJsonApi and AshGraphql routers
plug MyAppWeb.Router
end
If you're using AshJsonApi with a standalone Plug router (without Phoenix):
# lib/my_app/router.ex
defmodule MyApp.Router do
use Plug.Router
plug :match
plug :dispatch
get "/health" do
checks = %{database: database_ok?()}
status = if Enum.all?(checks, fn {_, v} -> v end), do: 200, else: 503
body = Jason.encode!(%{status: if(status == 200, do: "ok", else: "degraded"), checks: checks})
conn
|> put_resp_content_type("application/json")
|> send_resp(status, body)
end
defp database_ok? do
match?({:ok, _}, Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []))
end
end
Test it:
curl -s http://localhost:4000/health | jq .
# {
# "status": "ok",
# "checks": { "database": "ok", "ash_resources": "ok" }
# }
Step 2: Configure Vigilmon HTTP Monitors
Log in to Vigilmon → Monitors → 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: JSON:API Root
If you expose AshJsonApi, monitor the API root — it validates that your Ash resource routing is wired correctly:
| Field | Value |
|---|---|
| URL | https://yourapp.com/api/json |
| Method | GET |
| Expected status | 200 |
| Check interval | 1 minute |
Monitor 3: GraphQL Introspection (optional)
For AshGraphql deployments, a lightweight introspection query confirms the schema is serving:
| Field | Value |
|---|---|
| URL | https://yourapp.com/api/graphql |
| Method | POST |
| Body | {"query":"{__typename}"} |
| Expected body | "data" |
| Check interval | 5 minutes |
Step 3: Heartbeat for Ash Reactor Pipelines
Ash Reactor lets you define multi-step, transactional workflows. Long-running Reactor pipelines (nightly data syncs, report generation) can fail silently — the BEAM process exits cleanly but the work never completes.
# lib/my_app/reactors/nightly_sync_reactor.ex
defmodule MyApp.Reactors.NightlySyncReactor do
use Reactor
input :since
step :fetch_records do
argument :since, input(:since)
run fn %{since: since}, _ ->
MyApp.Accounts.User
|> Ash.Query.filter(updated_at > ^since)
|> Ash.read()
end
end
step :sync_to_external do
argument :records, result(:fetch_records)
run fn %{records: records}, _ ->
MyApp.ExternalSync.push(records)
end
end
step :ping_heartbeat do
argument :result, result(:sync_to_external)
run fn _, _ ->
url = Application.get_env(:my_app, :vigilmon)[:sync_heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, _} -> {:ok, :pinged}
{:error, reason} ->
require Logger
Logger.warning("Vigilmon heartbeat ping failed: #{inspect(reason)}")
{:ok, :ping_failed}
end
else
{:ok, :no_url_configured}
end
end
end
end
Trigger the Reactor from an Oban job or cron:
# lib/my_app/workers/nightly_sync_worker.ex
defmodule MyApp.Workers.NightlySyncWorker do
use Oban.Worker, queue: :default, max_attempts: 3
@impl Oban.Worker
def perform(%Oban.Job{}) do
since = DateTime.add(DateTime.utc_now(), -86_400, :second)
case Reactor.run(MyApp.Reactors.NightlySyncReactor, %{since: since}) do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}
end
end
end
In Vigilmon:
- Monitors → New Heartbeat Monitor
- Expected interval: 25 hours (daily sync with a 1-hour grace)
- Copy the ping URL and set in your environment:
VIGILMON_SYNC_HEARTBEAT_URL=https://vigilmon.online/api/heartbeat/xxxxxxxx
# config/runtime.exs
config :my_app, :vigilmon,
sync_heartbeat_url: System.get_env("VIGILMON_SYNC_HEARTBEAT_URL")
Step 4: Monitor Ash Policy Authorization Health
Ash's policy authorizer runs on every read and write. A misconfigured policy can cause your API to return blanket 403s — which looks like downtime from the outside. Add a monitor for an endpoint that requires authorization to detect policy failures:
# In your health check, optionally probe Ash policy evaluation
defp check_policy do
actor = %{role: :health_checker}
case Ash.read(MyApp.Accounts.User, actor: actor, action: :list, authorize?: true, page: [limit: 1]) do
{:ok, _} -> :ok
{:error, %Ash.Error.Forbidden{}} -> :ok # expected if health_checker role has no access
{:error, _} -> :error
end
rescue
_ -> :error
end
This check returns :ok for both "allowed" and "correctly forbidden" results, and :error only for unexpected failures — so the health endpoint won't false-alert on intentional authorization boundaries.
Step 5: Alert Channels
In Vigilmon → Alert Channels:
- Alert Channels → Email → add your on-call address
- Attach to all monitors
Slack
- Create a Slack Incoming Webhook for your
#alertschannel - Alert Channels → Webhook → paste the URL
- 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 database failure (stop Postgres or revoke connection)
# Expected: health returns 503 with "ash_resources": "error"
# Expected: Vigilmon alerts within 2 minutes
# 3. Trigger the Reactor manually and confirm the heartbeat is pinged
Reactor.run(MyApp.Reactors.NightlySyncReactor, %{since: ~U[2000-01-01 00:00:00Z]})
# 4. In Vigilmon, use "Test Alert" to confirm email and Slack delivery
Summary
| Monitor | What It Catches | |---|---| | HTTP health endpoint | Database failures, broken Ash data layers | | JSON:API root | Ash resource routing failures, bad deploys | | GraphQL introspection | Schema compilation errors, AshGraphql misconfiguration | | Reactor heartbeat | Silent pipeline failures, stalled Oban jobs |
Next Steps
- Add per-domain monitors if your Ash app serves multiple Phoenix scopes
- Use Vigilmon's response time graphs to catch slow Ash policy evaluations before they time out
- Add a heartbeat for each Ash Reactor that drives time-sensitive workflows
- Configure separate alert channels for data-layer failures vs. authorization failures
Ready to get started? Sign up for Vigilmon free — have your first monitor running in under 5 minutes.