tutorial

How to Monitor Req with Vigilmon

Req is Elixir's modern HTTP client with automatic retries and caching — but HTTP clients used for third-party integrations can mask failures through silent retries, poison caches, and misrouted requests. Here's how to monitor Req-based integrations with Vigilmon.

How to Monitor Req with Vigilmon

Req is Elixir's modern HTTP client — built on Finch, with automatic retries, response caching, compression, JSON encoding/decoding, and a composable plugin system baked in. It's the go-to client for Elixir applications that call third-party APIs, internal microservices, or webhooks. But Req's reliability features can mask real problems: automatic retries hide intermittent failures, response caching can serve stale data after an upstream change, and base URL configuration mistakes silently route requests to the wrong endpoint.

This tutorial sets up monitoring for applications that use Req as their HTTP client:

  • A health endpoint that exercises your Req-based integrations
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring for scheduled Req jobs
  • Request failure rate tracking via telemetry
  • Slack alerts when upstream APIs degrade

Why Monitor Req-based Integrations?

| Signal | What it catches | |---|---| | Upstream API availability | Third-party API outages, rate limiting, auth token expiry | | Retry rate | Intermittent failures hidden by automatic retries | | Cache freshness | Stale cached responses after upstream schema changes | | Request latency | Upstream slowdowns affecting your response times | | Error response rate | Non-retried 4xx errors, unexpected 5xx from APIs |

Req's retry and cache features are production lifesavers — and monitoring blind spots.


Step 1: Create a Req Client Module

Structure your Req usage as a named client with a shared base configuration:

# lib/my_app/http_client.ex
defmodule MyApp.HttpClient do
  @base_url Application.compile_env(:my_app, :external_api_url, "https://api.example.com")

  def new do
    Req.new(
      base_url: @base_url,
      retry: :transient,
      retry_delay: &retry_delay/1,
      max_retries: 3,
      receive_timeout: 10_000,
      connect_options: [timeout: 5_000]
    )
    |> Req.Request.append_request_steps(telemetry: &attach_telemetry/1)
  end

  def get(path, opts \\ []) do
    Req.get(new(), [url: path] ++ opts)
  end

  def post(path, body, opts \\ []) do
    Req.post(new(), [url: path, json: body] ++ opts)
  end

  defp retry_delay(n), do: Integer.pow(2, n) * 1000

  defp attach_telemetry(request) do
    start = System.monotonic_time()
    %{request | private: Map.put(request.private, :start_time, start)}
  end
end

Step 2: Add a Health Endpoint for Your Integrations

Create a health endpoint that makes a lightweight Req call to each external API your application depends on:

# lib/my_app_web/plugs/integrations_health.ex
defmodule MyAppWeb.Plugs.IntegrationsHealth do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health/integrations"} = conn, _opts) do
    checks = run_integration_checks()
    all_ok = Enum.all?(checks, fn {_name, status} -> status == :ok end)

    {http_status, body} = if all_ok do
      {200, %{status: "ok", checks: format_checks(checks)}}
    else
      {503, %{status: "degraded", checks: format_checks(checks)}}
    end

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

  def call(conn, _opts), do: conn

  defp run_integration_checks do
    [
      {"external_api", check_external_api()},
      {"payment_gateway", check_payment_gateway()}
    ]
  end

  defp check_external_api do
    case MyApp.HttpClient.get("/health") do
      {:ok, %{status: 200}} -> :ok
      _ -> :error
    end
  end

  defp check_payment_gateway do
    case Req.get("https://api.stripe.com/v1/charges",
           headers: [{"Authorization", "Bearer #{System.get_env("STRIPE_KEY")}"}],
           params: [limit: 1]) do
      {:ok, %{status: 200}} -> :ok
      _ -> :error
    end
  end

  defp format_checks(checks) do
    Map.new(checks, fn {name, status} -> {name, to_string(status)} end)
  end
end

Register the plug:

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

Test it:

curl http://localhost:4000/health/integrations
# => {"status":"ok","checks":{"external_api":"ok","payment_gateway":"ok"}}

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 integration health URL: https://yourapp.com/health/integrations.
  4. Set Check interval to 2 minutes.
  5. Set Expected HTTP status to 200.
  6. Under Advanced, add a Response body contains check: "status":"ok".
  7. Click Save.

Vigilmon now verifies all your Req-based integrations are reachable on every check.


Step 4: Track Req Request Metrics via Telemetry

Add a telemetry handler to capture Req request outcomes:

# lib/my_app/req_telemetry.ex
defmodule MyApp.ReqTelemetry do
  require Logger

  def setup do
    :telemetry.attach_many(
      "req-requests",
      [
        [:req, :request, :stop],
        [:req, :request, :exception]
      ],
      &handle_event/4,
      nil
    )
  end

  def handle_event([:req, :request, :stop], measurements, metadata, _config) do
    duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
    status = get_in(metadata, [:result, :status]) || 0

    if status >= 400 do
      Logger.warning("Req request failed: status=#{status} duration=#{duration_ms}ms url=#{metadata.url}")
    end

    if duration_ms > 5000 do
      Logger.warning("Slow Req request: #{duration_ms}ms url=#{metadata.url}")
    end

    :telemetry.execute(
      [:my_app, :req, :request],
      %{duration_ms: duration_ms, status: status},
      %{url: metadata[:url]}
    )
  end

  def handle_event([:req, :request, :exception], _measurements, metadata, _config) do
    Logger.error("Req request exception: #{inspect(metadata.reason)}")
  end
end

Attach in application.ex:

def start(_type, _args) do
  MyApp.ReqTelemetry.setup()
  # ...
end

Step 5: Heartbeat for Scheduled Req Jobs

Many applications use Req for scheduled jobs — polling external APIs, syncing data, sending webhooks. Monitor these with a Vigilmon heartbeat:

# lib/my_app/api_sync_worker.ex
defmodule MyApp.ApiSyncWorker do
  use GenServer

  @sync_interval :timer.minutes(15)
  @heartbeat_url "https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"

  def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)

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

  def handle_info(:sync, state) do
    case run_sync() do
      {:ok, _count} ->
        Req.get!(@heartbeat_url)
      {:error, reason} ->
        require Logger
        Logger.error("API sync failed: #{inspect(reason)}")
        # Do NOT ping heartbeat — Vigilmon will alert on the miss
    end

    schedule()
    {:noreply, state}
  end

  defp schedule, do: Process.send_after(self(), :sync, @sync_interval)

  defp run_sync do
    case MyApp.HttpClient.get("/api/v1/records", params: [since: last_sync_time()]) do
      {:ok, %{status: 200, body: %{"records" => records}}} ->
        MyApp.Records.upsert_all(records)
        {:ok, length(records)}
      {:ok, %{status: status}} ->
        {:error, {:http_error, status}}
      {:error, reason} ->
        {:error, reason}
    end
  end

  defp last_sync_time do
    DateTime.utc_now() |> DateTime.add(-15, :minute) |> DateTime.to_iso8601()
  end
end

Create the heartbeat in Vigilmon:

  1. Click Add MonitorCron / Heartbeat.
  2. Set expected interval to 15 minutes.
  3. Set grace period to 5 minutes.
  4. Copy the generated URL into @heartbeat_url.

Step 6: Set Up Alerts

  1. Go to Alert ChannelsAdd Channel.
  2. Choose Slack, Email, or PagerDuty.
  3. For integration health HTTP monitor: alert after 2 consecutive failures.
  4. For sync heartbeats: alert after first miss.
  5. Assign channels to all Req monitors.

For customer-facing features backed by third-party APIs, also create a Status Page:

  1. Go to Status PagesCreate Page.
  2. Add your integration health monitor under a label like "API Integrations".
  3. Share the public URL in your documentation.

Key Metrics to Watch

| Metric | Vigilmon feature | What to alert on | |---|---|---| | Integration availability | HTTP monitor on /health/integrations | Any 503 or timeout | | Scheduled sync completion | Heartbeat monitor | Missing for > 20 minutes | | API response time | Response time chart | P95 > 5s | | SSL certificate | Cert expiry monitor | Expires in < 14 days | | Retry storm | Telemetry + heartbeat gap | Repeated heartbeat delays |


Conclusion

Req makes HTTP in Elixir reliable and ergonomic — but its reliability features (retries, caching) need to be paired with observability so you know when your upstream APIs are actually struggling. A health endpoint that exercises your integrations, a heartbeat for scheduled jobs, and telemetry for request metrics give you the full picture. With these Vigilmon monitors in place, you'll catch third-party API degradation before it cascades into user-facing failures.

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 →