tutorial

How to Monitor HTTPoison with Vigilmon

Monitor Elixir services that use HTTPoison — track external API health, response times, request failure rates, and get alerted when dependencies go down.

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 :ssl errors
  • 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:

  1. Sign in at vigilmon.online
  2. Click New Monitor → HTTP for each critical dependency
  3. 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-Id headers to all HTTPoison requests and correlate them with Vigilmon alert timestamps
  • Consider wrapping HTTPoison with circuit breaker logic (:fuse or ExBreak) and monitoring circuit state

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 →