tutorial

How to Monitor Ash.GraphQL with Vigilmon

Ash.GraphQL auto-generates a GraphQL API from your Ash resources — but a single /graphql endpoint returning HTTP 200 can mask resolver failures, authorization bugs, and subscription issues. Here's how to monitor Ash.GraphQL end-to-end with Vigilmon.

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

  1. Log in to vigilmon.online and click Add Monitor.
  2. Set Type to HTTP / HTTPS.
  3. Enter your health URL: https://api.yourapp.com/health/graphql.
  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 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:

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

  1. Go to Alert ChannelsAdd Channel.
  2. Choose Slack, Email, or PagerDuty.
  3. Set alert thresholds: notify after 2 consecutive failures to avoid transient noise.
  4. Assign the alert channel to both your HTTP monitor and your heartbeat monitor.

For public APIs, also create a Status Page:

  1. Go to Status PagesCreate Page.
  2. Add your GraphQL health monitor and your heartbeat monitor.
  3. 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.

Monitor your app with Vigilmon

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

Start free →