How to Monitor Ash.GraphQL with Vigilmon
Ash.GraphQL is the GraphQL extension for the Ash Framework. It auto-generates queries, mutations, and subscriptions directly from your Ash resource definitions — no hand-written resolvers, no repetitive schema boilerplate. But GraphQL's single-endpoint architecture creates a monitoring blind spot: every request hits /graphql and returns HTTP 200, even when actions error out or authorization policies reject the caller. Standard uptime monitors declare everything healthy while your users see {"errors": [...]}.
This tutorial layers monitoring for an Ash.GraphQL API:
- A dedicated health action on an Ash resource exercised by a lightweight HTTP endpoint
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring to catch silent GraphQL error spikes
- Ash action telemetry for latency and failure alerting
- Slack notifications when the API degrades
Why Monitor Ash.GraphQL?
| Signal | What it catches |
|---|---|
| Schema availability | Deploy regressions that break resource loading |
| Action error rate | Policy denials, validation failures, DB errors |
| Resolver latency | Expensive aggregates, missing load calls |
| Authorization bugs | Policies silently rejecting valid requests |
| Subscription channels | PubSub failures, WebSocket transport issues |
A TCP check misses all of these — you need to execute actual GraphQL operations against your resources.
Step 1: Add a Health Action to an Ash Resource
Create a dedicated health read action that Vigilmon's HTTP monitor can exercise. The action verifies Ash internals, data-layer connectivity, and your API layer in one round trip.
# lib/my_app/monitoring/health.ex
defmodule MyApp.Monitoring.Health do
use Ash.Resource,
domain: MyApp.Monitoring,
extensions: [AshGraphql.Resource]
graphql do
type :health_result
queries do
read :health, :check
end
end
actions do
read :check do
primary? true
prepare fn query, _context ->
Ash.Query.load(query, [])
end
end
end
attributes do
attribute :status, :string, allow_nil?: false
attribute :checked_at, :utc_datetime_usec, allow_nil?: false
end
# Return a synthetic record — no database table needed
data_layer AshPostgres.DataLayer do
# Use :ets for an in-memory health resource if you prefer
end
end
For a simpler no-database approach, use Ash with an ETS data layer:
defmodule MyApp.Monitoring.Health do
use Ash.Resource,
domain: MyApp.Monitoring,
data_layer: Ash.DataLayer.Ets,
extensions: [AshGraphql.Resource]
graphql do
type :health_result
queries do
read :health, :check
end
end
actions do
read :check do
primary? true
manual MyApp.Monitoring.HealthRead
end
end
attributes do
attribute :id, :uuid, primary_key?: true, allow_nil?: false
attribute :status, :string, allow_nil?: false
attribute :checked_at, :utc_datetime_usec, allow_nil?: false
end
end
defmodule MyApp.Monitoring.HealthRead do
use Ash.Resource.ManualRead
def read(_query, _data_layer_query, _opts, _context) do
{:ok, [%{id: Ash.UUID.generate(), status: "ok", checked_at: DateTime.utc_now()}]}
end
end
Test the query:
curl -X POST http://localhost:4000/gql \
-H "Content-Type: application/json" \
-d '{"query": "{ health { status checkedAt } }"}'
# => {"data":{"health":{"status":"ok","checkedAt":"2026-07-04T10:00:00Z"}}}
Step 2: Add an HTTP Health Endpoint
Vigilmon monitors HTTP endpoints. Wrap the GraphQL health query in a Phoenix controller action so you get a clean, Vigilmon-targetable URL:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def graphql(conn, _params) do
case check_graphql_health() do
:ok ->
json(conn, %{status: "ok", layer: "ash_graphql"})
{:error, reason} ->
conn
|> put_status(503)
|> json(%{status: "error", reason: inspect(reason)})
end
end
defp check_graphql_health do
query = """
{ health { status checkedAt } }
"""
case Absinthe.run(query, MyAppWeb.Schema, context: %{actor: nil}) do
{:ok, %{"data" => %{"health" => %{"status" => "ok"}}}} -> :ok
{:ok, %{"errors" => errors}} -> {:error, errors}
{:error, reason} -> {:error, reason}
end
end
end
Add the route:
# lib/my_app_web/router.ex
scope "/health", MyAppWeb do
pipe_through :api
get "/graphql", HealthController, :graphql
end
GET /health/graphql now returns 200 when healthy, 503 on any Ash or GraphQL failure.
Step 3: Add Vigilmon Uptime Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health URL:
https://api.yourapp.com/health/graphql. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add a Response body contains check:
"status":"ok". - Click Save.
Vigilmon will alert you within one minute if Ash.GraphQL becomes unavailable.
Step 4: Monitor Action Error Rates with a Heartbeat
Ash actions can fail silently — the HTTP layer returns 200 with GraphQL errors in the body. Use Ash's telemetry events to track action error rates and stop sending heartbeats to Vigilmon when errors spike.
Attach a telemetry handler in your application:
# lib/my_app/ash_telemetry.ex
defmodule MyApp.AshTelemetry do
require Logger
def setup do
:telemetry.attach_many(
"ash-graphql-errors",
[
[:ash, :read, :stop],
[:ash, :create, :stop],
[:ash, :update, :stop],
[:ash, :destroy, :stop]
],
&handle_event/4,
nil
)
end
def handle_event([_domain, action, :stop], measurements, metadata, _config) do
case metadata do
%{error: error} when not is_nil(error) ->
:ets.update_counter(:ash_error_counts, action, {2, 1}, {action, 0})
_ ->
:ok
end
end
end
Create a heartbeat GenServer that pings Vigilmon only when error counts are within threshold:
# lib/my_app/graphql_heartbeat.ex
defmodule MyApp.GraphQLHeartbeat do
use GenServer
@interval :timer.minutes(1)
@error_threshold 5
@vigilmon_heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
:ets.new(:ash_error_counts, [:named_table, :public, :set])
schedule()
{:ok, nil}
end
def handle_info(:ping, state) do
total_errors = :ets.tab2list(:ash_error_counts)
|> Enum.reduce(0, fn {_, count}, acc -> acc + count end)
if total_errors < @error_threshold do
HTTPoison.get!(@vigilmon_heartbeat_url)
end
:ets.delete_all_objects(:ash_error_counts)
schedule()
{:noreply, state}
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
Add both to your supervision tree and attach telemetry in application.ex:
def start(_type, _args) do
MyApp.AshTelemetry.setup()
children = [
# ...
MyApp.GraphQLHeartbeat
]
Supervisor.start_link(children, strategy: :one_for_one)
end
To create the heartbeat monitor in Vigilmon:
- Click Add Monitor → Cron / Heartbeat.
- Set the expected interval to
1 minute. - Copy the generated URL into
@vigilmon_heartbeat_url.
Step 5: Monitor Authorization Latency
Ash policy evaluation adds latency to every action. Track it with a custom telemetry span:
defmodule MyApp.PolicyTelemetry do
def setup do
:telemetry.attach(
"ash-policy-latency",
[:ash, :policy, :stop],
fn _event, %{duration: duration}, %{resource: resource}, _config ->
ms = System.convert_time_unit(duration, :native, :millisecond)
if ms > 500 do
Logger.warning("Slow Ash policy on #{inspect(resource)}: #{ms}ms")
end
end,
nil
)
end
end
Step 6: Set Up Alerts
In Vigilmon, configure alerts for your Ash.GraphQL monitors:
- Go to Alert Channels → Add Channel.
- Choose Slack, Email, or PagerDuty.
- Set alert thresholds: notify after 2 consecutive failures to avoid transient noise.
- Assign the alert channel to both your HTTP monitor and your heartbeat monitor.
For public APIs, also create a Status Page:
- Go to Status Pages → Create Page.
- Add your GraphQL health monitor and your heartbeat monitor.
- Share the URL with API consumers so they can self-serve status checks.
Key Metrics to Watch
| Metric | Vigilmon feature | Alert threshold |
|---|---|---|
| Schema availability | HTTP monitor on /health/graphql | Any 5xx or timeout |
| Action error rate | Heartbeat (stops when errors spike) | Missing for > 2 min |
| Response time | Response time chart | P95 > 1s over 5 minutes |
| SSL certificate | Cert expiry monitor | Expires in < 14 days |
| Policy evaluation latency | Custom telemetry + heartbeat | > 500ms average |
Conclusion
Ash.GraphQL's resource-driven approach eliminates resolver boilerplate, but you still need visibility into what happens at runtime. A three-layer strategy — HTTP availability check, heartbeat that pauses on error spikes, and Ash telemetry for latency — gives you confidence that your auto-generated GraphQL API is healthy end-to-end.
Get started free at vigilmon.online.