tutorial

How to Monitor Ex_AWS with Vigilmon

Add uptime monitoring, AWS API health checks, and alerts to your Ex_AWS-powered Elixir application — so S3 outages, SQS queue stalls, and credential failures don't go undetected.

How to Monitor Ex_AWS with Vigilmon

Ex_AWS is the go-to Elixir library for interacting with AWS services — S3, DynamoDB, SQS, Lambda, SNS, and more. It wraps the AWS API into composable, pipe-friendly operations that feel native to Elixir. But when AWS has a regional outage, your credentials expire, or an S3 bucket policy changes, Ex_AWS calls start failing silently. Your application keeps responding 200 while background uploads fail, SQS messages pile up unprocessed, and critical data never reaches its destination.

External monitoring catches what AWS CloudWatch misses at the application layer. This tutorial covers:

  • A health endpoint that validates Ex_AWS connectivity across key services
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring to detect when AWS-dependent jobs stall
  • Slack alerts and a status page

Step 1: Add Ex_AWS to your project

# mix.exs
defp deps do
  [
    {:ex_aws, "~> 2.5"},
    {:ex_aws_s3, "~> 2.5"},
    {:ex_aws_sqs, "~> 3.4"},
    {:hackney, "~> 1.9"},
    {:jason, "~> 1.2"},
    {:req, "~> 0.4"}   # for heartbeat pings
  ]
end

Configure credentials:

# config/config.exs
config :ex_aws,
  access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
  secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role],
  region: {:system, "AWS_REGION"}

config :ex_aws, :s3,
  scheme: "https://",
  host: "s3.amazonaws.com",
  region: {:system, "AWS_REGION"}

The [{:system, "..."}, :instance_role] fallback chain tries the environment variable first, then falls back to EC2/ECS instance role credentials — useful for production deployments.


Step 2: Build a health endpoint that validates Ex_AWS connectivity

Add a plug that exercises each AWS service your application depends on:

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn
  require Logger

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = run_checks()
    status = if all_ok?(checks), do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(checks))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp run_checks do
    s3 = s3_check()
    sqs = sqs_check()

    overall_ok = s3.status == "ok" and sqs.status == "ok"

    %{
      status: status_label(overall_ok),
      s3: s3,
      sqs: sqs
    }
  end

  defp s3_check do
    bucket = Application.get_env(:my_app, :aws)[:health_bucket]

    case ExAws.S3.head_bucket(bucket) |> ExAws.request() do
      {:ok, _} ->
        %{status: "ok", bucket: bucket}

      {:error, {:http_error, 403, _}} ->
        # 403 means the bucket exists and credentials are valid (just no list permission)
        %{status: "ok", bucket: bucket, note: "credentials valid, list restricted"}

      {:error, {:http_error, 404, _}} ->
        %{status: "error", reason: "bucket not found: #{bucket}"}

      {:error, {:http_error, code, _}} ->
        %{status: "error", reason: "HTTP #{code}"}

      {:error, reason} ->
        %{status: "error", reason: inspect(reason)}
    end
  end

  defp sqs_check do
    queue_url = Application.get_env(:my_app, :aws)[:health_queue_url]

    case ExAws.SQS.get_queue_attributes(queue_url, [:approximate_number_of_messages])
         |> ExAws.request() do
      {:ok, %{body: %{attributes: attrs}}} ->
        depth = attrs |> Map.get("ApproximateNumberOfMessages", "0") |> String.to_integer()
        %{status: "ok", queue_depth: depth}

      {:error, reason} ->
        %{status: "error", reason: inspect(reason)}
    end
  end

  defp all_ok?(checks), do: checks.status == "ok"

  defp status_label(true), do: "ok"
  defp status_label(false), do: "degraded"
end

Register the plug in your endpoint:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router

Test it locally (requires valid AWS credentials in your environment):

mix phx.server
curl http://localhost:4000/health | jq .
# {
#   "status": "ok",
#   "s3": {"status": "ok", "bucket": "my-app-assets"},
#   "sqs": {"status": "ok", "queue_depth": 3}
# }

A 503 with "status": "degraded" tells you which AWS service is failing — before Vigilmon even alerts you.


Step 3: Monitor the health endpoint with Vigilmon

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. Enable keyword check: body must contain "status":"ok"
  6. Save

The keyword check is critical for AWS monitoring. AWS API calls can succeed at the transport level (HTTP 200 from your web server) while the actual AWS operation returns an error body — the keyword match catches that case.


Step 4: Heartbeat monitoring for AWS-dependent background jobs

If you process SQS messages, upload files to S3, or run DynamoDB operations in background workers, a heartbeat proves these jobs ran and their AWS calls succeeded:

# lib/my_app/workers/aws_heartbeat_worker.ex
defmodule MyApp.Workers.AwsHeartbeatWorker do
  use GenServer
  require Logger

  @interval :timer.minutes(5)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  def init(state) do
    schedule()
    {:ok, state}
  end

  def handle_info(:run, state) do
    run_check()
    schedule()
    {:noreply, state}
  end

  defp run_check do
    bucket = Application.get_env(:my_app, :aws)[:health_bucket]

    # Write a tiny sentinel object to confirm S3 write access
    result =
      ExAws.S3.put_object(bucket, ".vigilmon-heartbeat", "ok", [
        {:content_type, "text/plain"}
      ])
      |> ExAws.request()

    case result do
      {:ok, _} ->
        Logger.debug("AWS heartbeat: S3 write OK")
        ping_vigilmon()

      {:error, reason} ->
        Logger.error("AWS heartbeat S3 write failed: #{inspect(reason)}")
    end
  end

  defp ping_vigilmon do
    url = Application.get_env(:my_app, :vigilmon)[:heartbeat_url]

    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, %{status: 200}} -> :ok
        error -> Logger.warning("Vigilmon heartbeat ping failed: #{inspect(error)}")
      end
    end
  end

  defp schedule, do: Process.send_after(self(), :run, @interval)
end

Add it to your supervision tree:

children = [
  MyApp.Repo,
  MyAppWeb.Endpoint,
  MyApp.Workers.AwsHeartbeatWorker
]

In Vigilmon, create a Heartbeat Monitor:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 10 minutes
  3. Copy the ping URL
  4. Set it as VIGILMON_HEARTBEAT_URL in your environment

If credentials expire, the S3 bucket policy changes, or the worker crashes, the heartbeat stops — and Vigilmon alerts you within one missed interval.


Step 5: Monitor SQS queue depth for message buildup

A growing SQS queue often signals a broken consumer. Expose queue depth in your health response and alert when it exceeds a threshold:

# Enhanced SQS check with threshold
defp sqs_check do
  queue_url = Application.get_env(:my_app, :aws)[:health_queue_url]
  threshold = Application.get_env(:my_app, :aws)[:queue_depth_alert_threshold] || 1000

  case ExAws.SQS.get_queue_attributes(queue_url, [:approximate_number_of_messages])
       |> ExAws.request() do
    {:ok, %{body: %{attributes: attrs}}} ->
      depth = attrs |> Map.get("ApproximateNumberOfMessages", "0") |> String.to_integer()
      status = if depth < threshold, do: "ok", else: "warning"
      %{status: status, queue_depth: depth, threshold: threshold}

    {:error, reason} ->
      %{status: "error", reason: inspect(reason)}
  end
end

Set a Vigilmon keyword alert to fire when the body contains "status":"warning" — catching queue buildup before it becomes a data processing backlog.


Step 6: Credential expiry detection

IAM credentials with fixed expiry dates are a common cause of sudden AWS failures. Add a check for credential metadata when using temporary credentials:

defp credentials_check do
  case ExAws.Config.new(:s3) |> ExAws.Config.retrieve_credentials() do
    {:ok, %{expiration: expiration}} when is_binary(expiration) ->
      {:ok, expires_at, _} = DateTime.from_iso8601(expiration)
      now = DateTime.utc_now()
      hours_remaining = DateTime.diff(expires_at, now, :hour)

      status = cond do
        hours_remaining <= 0 -> "error"
        hours_remaining <= 1 -> "warning"
        true -> "ok"
      end

      %{status: status, expires_in_hours: hours_remaining}

    {:ok, _} ->
      # Static credentials — no expiry to track
      %{status: "ok", note: "static credentials"}

    {:error, reason} ->
      %{status: "error", reason: inspect(reason)}
  end
end

This surfaces credential expiry in your health response so Vigilmon keyword alerts fire before the credentials actually expire.


Step 7: Slack alerts and status page

Slack alerts:

  1. In Vigilmon, go to Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. Enable the channel on your AWS health monitor and heartbeat monitor

When S3 goes unreachable:

🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
2 minutes ago

When the SQS queue is backing up:

🟡 KEYWORD ALERT: yourdomain.com/health
Body contains "status":"warning"
queue_depth: 1247

Status page:

  1. Go to Status Pages → New Status Page
  2. Add your AWS health monitor and heartbeat monitor
  3. Share the URL with your on-call team

What you've built

| What | How | |------|-----| | Health endpoint | Plug with S3 head-bucket + SQS queue attribute checks | | Keyword check | Vigilmon keyword match on "status":"ok" | | Uptime monitoring | Vigilmon HTTP monitor → /health | | AWS write liveness | S3 sentinel object write + Vigilmon heartbeat | | Queue buildup alerting | SQS depth threshold in health response | | Credential expiry detection | IAM credential expiry check in health endpoint | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Ex_AWS keeps your application connected to the cloud. Vigilmon tells you when the cloud doesn't cooperate.


Next steps

  • Add per-service monitors for DynamoDB, SNS, or Lambda if your application uses them
  • Use Vigilmon's response time history to detect AWS API latency increases before they cause timeouts
  • Set up separate heartbeats for each SQS consumer queue if they have independent SLAs

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 →