How to Monitor Ash.JsonApi with Vigilmon
Ash.JsonApi is the JSON:API 1.0 extension for the Ash Framework. It auto-generates standards-compliant REST endpoints from your Ash resource definitions — filtering, sorting, pagination, and relationship inclusion all work without writing controllers or serializers. But JSON:API's richness introduces monitoring surface area that simple uptime checks miss: filtered queries that return empty sets when they should return data, relationship includes that silently fail, and paginated responses that never complete.
This tutorial builds end-to-end monitoring for an Ash.JsonApi service:
- A dedicated health endpoint that exercises the JSON:API layer
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for silent API degradation
- JSON:API response body validation to catch empty-when-not-empty failures
- Slack alerts when the API goes down
Why Monitor Ash.JsonApi?
| Signal | What it catches |
|---|---|
| Endpoint availability | Deploy regressions that break routing |
| Filter correctness | Queries returning wrong data sets |
| Relationship includes | Missing or broken data-layer joins |
| Pagination integrity | Incorrect meta.total or links.next |
| Authorization policies | Policies silently filtering all records |
A plain HTTP 200 check misses every one of these — you need to validate response structure, not just status codes.
Step 1: Add a Health Resource
Create a lightweight Ash resource specifically for health checking. It should exercise your Ash domain, data layer, and JSON:API serialization in a single request.
# lib/my_app/monitoring/health_check.ex
defmodule MyApp.Monitoring.HealthCheck do
use Ash.Resource,
domain: MyApp.Monitoring,
data_layer: Ash.DataLayer.Ets,
extensions: [AshJsonApi.Resource]
json_api do
type "health_checks"
routes do
base "/health"
index :list
end
end
actions do
read :list do
primary? true
manual MyApp.Monitoring.HealthCheckRead
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
attribute :version, :string, allow_nil?: false
end
end
defmodule MyApp.Monitoring.HealthCheckRead do
use Ash.Resource.ManualRead
def read(_query, _data_layer_query, _opts, _context) do
record = %{
id: Ash.UUID.generate(),
status: "ok",
checked_at: DateTime.utc_now(),
version: Application.spec(:my_app, :vsn) |> to_string()
}
{:ok, [record]}
end
end
Register it in your domain:
defmodule MyApp.Monitoring do
use Ash.Domain, extensions: [AshJsonApi.Domain]
resources do
resource MyApp.Monitoring.HealthCheck
end
end
Mount the router in your Phoenix endpoint:
# lib/my_app_web/router.ex
scope "/api" do
pipe_through :api
forward "/health", AshJsonApi.Router, domain: MyApp.Monitoring
end
Test the endpoint:
curl http://localhost:4000/api/health \
-H "Accept: application/vnd.api+json"
# => {
# "data": [{
# "type": "health_checks",
# "id": "...",
# "attributes": {"status": "ok", "version": "1.0.0", "checkedAt": "..."}
# }],
# "meta": {"total": 1}
# }
Step 2: Add a Thin HTTP Health Wrapper
JSON:API responses require the Accept: application/vnd.api+json header, which complicates Vigilmon's body-contains checks. Add a simpler JSON wrapper alongside your JSON:API routes:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def jsonapi(conn, _params) do
case check_jsonapi_health() do
{:ok, version} ->
json(conn, %{status: "ok", version: version})
{:error, reason} ->
conn
|> put_status(503)
|> json(%{status: "error", reason: inspect(reason)})
end
end
defp check_jsonapi_health do
case MyApp.Monitoring.HealthCheck |> Ash.read(domain: MyApp.Monitoring) do
{:ok, [%{status: "ok", version: v} | _]} -> {:ok, v}
{:ok, []} -> {:error, "no records returned"}
{:error, error} -> {:error, error}
end
end
end
# In your router
get "/health/jsonapi", HealthController, :jsonapi
GET /health/jsonapi returns {"status":"ok","version":"..."} when healthy and 503 on failure — clean enough for Vigilmon's body-contains assertion.
Step 3: Add Vigilmon Uptime Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health endpoint URL:
https://api.yourapp.com/health/jsonapi. - 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 now pings your JSON:API health wrapper every minute and alerts you on any failure.
Step 4: Validate Relationship Includes with a Heartbeat
Ash.JsonApi's include parameter triggers data-layer joins. These can silently fail — returning empty relationships instead of an error. Monitor them with a periodic probe that checks a known-good include:
# lib/my_app/jsonapi_heartbeat.ex
defmodule MyApp.JsonApiHeartbeat do
use GenServer
require Logger
@interval :timer.minutes(2)
@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: schedule() |> then(fn _ -> {:ok, nil} end)
def handle_info(:ping, state) do
case probe_api() do
:ok ->
HTTPoison.get!(@vigilmon_heartbeat_url)
Logger.info("JsonApi heartbeat: ok")
{:error, reason} ->
Logger.error("JsonApi heartbeat skipped: #{inspect(reason)}")
end
schedule()
{:noreply, state}
end
defp probe_api do
# Exercise a real resource with a filter to validate the full stack
url = "http://localhost:4000/api/your_resource?filter[active]=true&page[size]=1"
case HTTPoison.get(url, [{"Accept", "application/vnd.api+json"}]) do
{:ok, %{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, %{"data" => data}} when is_list(data) -> :ok
_ -> {:error, :invalid_body}
end
{:ok, %{status_code: code}} ->
{:error, {:http_error, code}}
{:error, reason} ->
{:error, reason}
end
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
To create the heartbeat monitor in Vigilmon:
- Click Add Monitor → Cron / Heartbeat.
- Set the expected interval to
2 minutes. - Copy the generated heartbeat URL into
@vigilmon_heartbeat_url.
Step 5: Monitor Ash Query Performance
Ash emits telemetry events for all data-layer operations. Attach handlers to track slow JSON:API queries:
# lib/my_app/jsonapi_telemetry.ex
defmodule MyApp.JsonApiTelemetry do
require Logger
@slow_query_ms 500
def setup do
:telemetry.attach_many(
"ash-jsonapi-queries",
[
[:ash, :read, :stop],
[:ash, :create, :stop],
[:ash, :update, :stop]
],
&handle_event/4,
nil
)
end
def handle_event([_, action, :stop], %{duration: duration}, %{resource: resource}, _) do
ms = System.convert_time_unit(duration, :native, :millisecond)
if ms > @slow_query_ms do
Logger.warning("[Ash.JsonApi] Slow #{action} on #{inspect(resource)}: #{ms}ms")
end
end
end
Attach it in application.ex:
def start(_type, _args) do
MyApp.JsonApiTelemetry.setup()
# ...
end
Step 6: Set Up Alerts
Configure Vigilmon alert channels for your Ash.JsonApi monitors:
- Go to Alert Channels → Add Channel.
- Add a Slack webhook URL or configure Email / PagerDuty.
- Set Alert after 2 consecutive failures to avoid single transient errors.
- Assign the channel to both your HTTP monitor and heartbeat monitor.
For customer-facing APIs, publish a Status Page:
- Go to Status Pages → Create Page.
- Add your JSON:API health monitor.
- Share the public URL with integrations partners and API consumers.
Key Metrics to Watch
| Metric | Vigilmon feature | Alert threshold |
|---|---|---|
| Endpoint availability | HTTP monitor on /health/jsonapi | Any non-200 or timeout |
| Include/filter correctness | Heartbeat (stops on probe failure) | Missing for > 4 min |
| Response time | Response time chart | P95 > 800ms |
| SSL certificate | Cert expiry monitor | Expires in < 14 days |
| Data-layer query latency | Ash telemetry + logs | > 500ms per query |
Conclusion
Ash.JsonApi dramatically reduces the code needed for a standards-compliant REST API, but the richness of JSON:API — filters, includes, pagination — also means more failure modes to monitor. Combining an HTTP availability check with a heartbeat probe that validates filtering and includes gives you confidence that the full stack is working, not just that the endpoint responds.
Get started free at vigilmon.online.