tutorial

How to Monitor Ash.JsonApi with Vigilmon

Ash.JsonApi automatically exposes your Ash resources as JSON:API 1.0 endpoints — but pagination, sparse fieldsets, and relationship inclusion make it easy for subtle failures to hide behind a healthy-looking HTTP 200. Here's how to monitor Ash.JsonApi with Vigilmon.

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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your health endpoint URL: https://api.yourapp.com/health/jsonapi.
  4. Set Check interval to 1 minute.
  5. Set Expected HTTP status to 200.
  6. Under Advanced, add a Response body contains check: "status":"ok".
  7. 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:

  1. Click Add MonitorCron / Heartbeat.
  2. Set the expected interval to 2 minutes.
  3. 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:

  1. Go to Alert ChannelsAdd Channel.
  2. Add a Slack webhook URL or configure Email / PagerDuty.
  3. Set Alert after 2 consecutive failures to avoid single transient errors.
  4. Assign the channel to both your HTTP monitor and heartbeat monitor.

For customer-facing APIs, publish a Status Page:

  1. Go to Status PagesCreate Page.
  2. Add your JSON:API health monitor.
  3. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →