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
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute
- Enable keyword check: body must contain
"status":"ok" - 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:
- Click New Monitor → Heartbeat
- Set expected interval: 10 minutes
- Copy the ping URL
- Set it as
VIGILMON_HEARTBEAT_URLin 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:
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- 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:
- Go to Status Pages → New Status Page
- Add your AWS health monitor and heartbeat monitor
- 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.