How to Monitor HTTPoison with Vigilmon
HTTPoison is one of the most widely-used HTTP clients in the Elixir ecosystem. Built on top of Hackney, it's the go-to choice for making HTTP requests from Elixir applications — calling external APIs, fetching webhook payloads, integrating with payment gateways, and talking to microservices.
When your application depends on external HTTP services through HTTPoison, any one of those dependencies can fail. External APIs go down, rate limits kick in, SSL certificates expire, and network partitions happen. This tutorial shows you how to add observability to HTTPoison-based integrations with Vigilmon — so you know about external failures before your users do.
Why Monitor HTTPoison?
HTTPoison makes it easy to call external APIs, but external APIs are outside your control:
- Third-party APIs go down — payment processors, email services, SMS gateways
- Rate limits return 429s — your integration silently stops processing
- SSL certificate changes break verification — connections fail with cryptic
:sslerrors - Response format changes — 200 responses that now contain error payloads
- Latency spikes — slow external APIs back-pressure your Elixir processes
You need monitoring at two layers: the external services your app calls via HTTPoison, and the health of your own application's HTTP-facing endpoints.
Key Metrics to Track
| Metric | Why It Matters | |--------|---------------| | External API uptime | Your integration is only as reliable as the API it calls | | Response time (p50/p95/p99) | Latency spikes cause GenServer mailbox buildup | | HTTP status code distribution | Rising 429s, 503s, or 5xxs signal upstream degradation | | Request timeout rate | Timeouts block Elixir processes if not handled async | | Integration heartbeat | Confirms periodic jobs using HTTPoison are still running |
Step 1: Wrap HTTPoison with Telemetry
The cleanest way to track HTTPoison metrics is to wrap it in a module that emits telemetry events:
defmodule MyApp.HTTP do
require Logger
@default_timeout 10_000
def get(url, headers \\ [], opts \\ []) do
request(:get, url, "", headers, opts)
end
def post(url, body, headers \\ [], opts \\ []) do
request(:post, url, body, headers, opts)
end
defp request(method, url, body, headers, opts) do
timeout = Keyword.get(opts, :timeout, @default_timeout)
start = System.monotonic_time(:millisecond)
result = HTTPoison.request(method, url, body, headers,
recv_timeout: timeout,
timeout: timeout
)
duration = System.monotonic_time(:millisecond) - start
emit_telemetry(method, url, result, duration)
result
end
defp emit_telemetry(method, url, result, duration) do
{status, code} = case result do
{:ok, %{status_code: code}} -> {:ok, code}
{:error, %HTTPoison.Error{reason: reason}} -> {:error, reason}
end
:telemetry.execute(
[:my_app, :http, :request],
%{duration: duration},
%{method: method, url: sanitize_url(url), status: status, status_code: code}
)
end
defp sanitize_url(url) do
# Strip query params to avoid logging secrets
url |> URI.parse() |> Map.put(:query, nil) |> URI.to_string()
end
end
Attach a telemetry handler to log slow or failed requests:
defmodule MyApp.HTTPTelemetry do
require Logger
def attach do
:telemetry.attach(
"http-request-logger",
[:my_app, :http, :request],
&handle_event/4,
nil
)
end
def handle_event([:my_app, :http, :request], %{duration: duration}, meta, _) do
if duration > 5_000 do
Logger.warning("Slow HTTP request: #{meta.method} #{meta.url} took #{duration}ms")
end
if meta.status == :error or (is_integer(meta.status_code) and meta.status_code >= 500) do
Logger.error("HTTP error: #{meta.method} #{meta.url} → #{inspect(meta.status_code)}")
end
end
end
Call MyApp.HTTPTelemetry.attach() in your application.ex start/2.
Step 2: Add a Health Endpoint That Checks External APIs
A health endpoint that probes your HTTPoison dependencies tells Vigilmon whether the integration is working end-to-end:
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 = %{
database: check_database(),
stripe_api: check_stripe(),
sendgrid_api: check_sendgrid()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{status: status_label(status), checks: format_checks(checks)}))
|> halt()
end
def call(conn, _opts), do: conn
defp check_stripe do
case HTTPoison.get("https://api.stripe.com/v1/balance",
[{"Authorization", "Bearer #{stripe_key()}"}],
recv_timeout: 5_000, timeout: 5_000) do
{:ok, %{status_code: 200}} -> :ok
{:ok, %{status_code: 401}} -> :ok # key works, auth expected without full key
_ -> :error
end
rescue
_ -> :error
end
defp check_sendgrid do
case HTTPoison.get("https://api.sendgrid.com/v3/user/profile",
[{"Authorization", "Bearer #{sendgrid_key()}"}],
recv_timeout: 5_000, timeout: 5_000) do
{:ok, %{status_code: code}} when code in 200..299 -> :ok
_ -> :error
end
rescue
_ -> :error
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp format_checks(checks) do
Map.new(checks, fn {k, v} -> {k, Atom.to_string(v)} end)
end
defp status_label(200), do: "ok"
defp status_label(_), do: "degraded"
defp stripe_key, do: Application.get_env(:my_app, :stripe)[:secret_key]
defp sendgrid_key, do: Application.get_env(:my_app, :sendgrid)[:api_key]
end
Step 3: Monitor External APIs Directly with Vigilmon
Don't rely only on your app's health endpoint — monitor the external services directly. If Stripe is down, you want to know before your health check even runs:
- Sign in at vigilmon.online
- Click New Monitor → HTTP for each critical dependency
- Configure:
| Monitor Name | URL | Keyword Check |
|---|---|---|
| Stripe API | https://status.stripe.com/ | "All Systems Operational" |
| SendGrid | https://status.sendgrid.com/ | "All Systems Operational" |
| Twilio | https://status.twilio.com/ | "All Systems Operational" |
| Your app /health | https://yourdomain.com/health | "ok" |
Set intervals to 1 minute for payment processors and 5 minutes for lower-criticality dependencies.
Step 4: Heartbeat Monitoring for HTTPoison-Powered Workers
If you have background jobs that use HTTPoison to push data to external services, add heartbeat monitoring:
defmodule MyApp.WebhookDispatcher do
use GenServer
require Logger
@interval :timer.minutes(5)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_dispatch()
{:ok, state}
end
@impl true
def handle_info(:dispatch, state) do
pending = MyApp.Webhooks.pending_events()
results = Enum.map(pending, fn event ->
HTTPoison.post(event.endpoint, Jason.encode!(event.payload),
[{"Content-Type", "application/json"}],
recv_timeout: 10_000
)
end)
failures = Enum.count(results, fn
{:ok, %{status_code: code}} when code >= 200 and code < 300 -> false
_ -> true
end)
if failures == 0 do
ping_heartbeat()
else
Logger.error("Webhook dispatcher: #{failures}/#{length(results)} deliveries failed")
end
schedule_dispatch()
{:noreply, state}
end
defp schedule_dispatch, do: Process.send_after(self(), :dispatch, @interval)
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:dispatcher_heartbeat_url]
if url, do: HTTPoison.get(url, [], recv_timeout: 5_000)
end
end
In Vigilmon, create a Heartbeat Monitor with a 5-minute interval for the dispatcher.
Step 5: Set Up Alerts
Go to Notifications → New Channel in Vigilmon and configure Slack or PagerDuty:
Recommended alert escalation:
- Payment processor (Stripe) down → page on-call immediately
- Email service (SendGrid) down → alert dev team, medium priority
- Your app health endpoint degraded → page on-call immediately
- Webhook dispatcher heartbeat missed → alert dev team
Example Slack notification:
🔴 DOWN: Stripe API Status
URL: https://status.stripe.com
Keyword "All Systems Operational" not found
Detected from: EU-West, US-East
Alert created: 3 minutes ago
Step 6: Response Time History
HTTPoison requests to external APIs should be fast. Use Vigilmon's response time history on your /health monitor to detect latency regressions:
- A spike in response time on the health endpoint often indicates one of the external API checks timing out
- Compare Vigilmon response time graphs against the timestamps of external API incidents
- Set a slow response alert in Vigilmon (e.g. alert if response time > 3 seconds) to catch upstream slowdowns before they cause timeouts in your app
What You've Built
| What | How |
|------|-----|
| HTTPoison telemetry | Custom wrapper emitting :telemetry events |
| Slow request logging | Telemetry handler with duration threshold |
| External API health checks | HTTPoison probes in /health plug |
| Direct dependency monitoring | Vigilmon HTTP monitors on vendor status pages |
| Worker liveness | Heartbeat ping in GenServer dispatch loop |
| Slack alerts | Vigilmon notification channel |
| Response time tracking | Vigilmon response time history |
HTTPoison connects your app to the world. Vigilmon tells you when the world isn't reachable.
Next Steps
- Add separate Vigilmon monitors for each external API endpoint your app uses (not just status pages)
- Use Vigilmon's multi-region checks to distinguish network-path failures from true API outages
- Add
X-Request-Idheaders to all HTTPoison requests and correlate them with Vigilmon alert timestamps - Consider wrapping HTTPoison with circuit breaker logic (
:fuseorExBreak) and monitoring circuit state
Get started free at vigilmon.online.