How to Monitor Exile with Vigilmon
Exile is an Elixir library for running external OS processes with demand-driven streaming I/O. Unlike System.cmd/3, Exile streams stdin and stdout without buffering the entire output in memory — making it the right tool for wrapping long-running CLI tools like FFmpeg, ImageMagick, and other media processing utilities.
But Exile is only as reliable as the external processes it wraps. A missing FFmpeg binary, an OS process that exits without producing expected output, or a leaked process that accumulates over time can silently degrade your media pipeline. External monitoring surfaces what Exile's internal error handling can't: that the entire execution path works end-to-end.
This tutorial covers:
- A health endpoint that validates external process execution via Exile
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring to detect when media processing pipelines stall
- Slack alerts and a status page
Step 1: Add Exile to your project
# mix.exs
defp deps do
[
{:exile, "~> 0.10"},
{:req, "~> 0.4"} # for heartbeat pings
]
end
Install any external tools your pipeline depends on:
# Debian/Ubuntu
apt-get install -y ffmpeg imagemagick
# Verify
ffmpeg -version
# ffmpeg version 6.x ...
convert --version
# Version: ImageMagick 7.x ...
A basic Exile usage — piping data through FFmpeg:
{:ok, data} =
Exile.stream!(~w[ffmpeg -i /path/to/input.mp4 -f mp4 pipe:1],
input: File.stream!("/path/to/input.mp4", [], 65536)
)
|> Enum.into([])
|> then(fn chunks -> {:ok, IO.iodata_to_binary(chunks)} end)
Step 2: Build a health endpoint that validates external process execution
Add a plug that runs a short Exile probe and reports the result:
# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = run_checks()
status = if checks.status == "ok", 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
exile = exile_check()
ffmpeg = ffmpeg_check()
all_ok = exile.status == "ok" and ffmpeg.status == "ok"
%{
status: if(all_ok, do: "ok", else: "degraded"),
exile: exile,
ffmpeg: ffmpeg
}
end
defp exile_check do
start = System.monotonic_time(:millisecond)
result =
try do
# Run a trivial command to verify Exile itself works
output =
Exile.stream!(~w[echo health-ok])
|> Enum.into([])
|> IO.iodata_to_binary()
|> String.trim()
elapsed = System.monotonic_time(:millisecond) - start
if output == "health-ok" do
%{status: "ok", latency_ms: elapsed}
else
%{status: "error", reason: "unexpected output: #{output}"}
end
rescue
e -> %{status: "error", reason: Exception.message(e)}
end
result
end
defp ffmpeg_check do
start = System.monotonic_time(:millisecond)
result =
try do
# Check FFmpeg is available and reports a valid version string
output =
Exile.stream!(~w[ffmpeg -version])
|> Enum.into([])
|> IO.iodata_to_binary()
elapsed = System.monotonic_time(:millisecond) - start
if String.contains?(output, "ffmpeg version") do
%{status: "ok", latency_ms: elapsed}
else
%{status: "error", reason: "unexpected ffmpeg output"}
end
rescue
e -> %{status: "error", reason: Exception.message(e)}
end
result
end
end
Register the plug before your router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
mix phx.server
curl http://localhost:4000/health | jq .
# {
# "status": "ok",
# "exile": {"status": "ok", "latency_ms": 12},
# "ffmpeg": {"status": "ok", "latency_ms": 85}
# }
Step 3: Track media processing metrics with telemetry
Wrap Exile calls to emit telemetry on each process execution:
# lib/my_app/media_processor.ex
defmodule MyApp.MediaProcessor do
require Logger
def transcode(input_path, output_path, format) do
start = System.monotonic_time(:millisecond)
result =
try do
Exile.stream!(
~w[ffmpeg -i #{input_path} -f #{format} #{output_path}],
stderr_to_console: false
)
|> Stream.run()
{:ok, output_path}
rescue
e -> {:error, Exception.message(e)}
end
elapsed = System.monotonic_time(:millisecond) - start
status = if match?({:ok, _}, result), do: "ok", else: "error"
:telemetry.execute(
[:my_app, :exile, :transcode],
%{duration_ms: elapsed},
%{format: format, status: status}
)
if status == "error" do
Logger.error("[Exile] transcode failed in #{elapsed}ms for #{format}: #{inspect(result)}")
end
result
end
end
Attach a telemetry handler for slow transcodes:
# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
require Logger
@slow_threshold_ms 30_000
def setup do
:telemetry.attach(
"exile-transcode-logger",
[:my_app, :exile, :transcode],
&handle_event/4,
nil
)
end
def handle_event(_event, %{duration_ms: ms}, %{format: fmt, status: status}, _cfg) do
if ms >= @slow_threshold_ms do
Logger.warning("[Exile slow transcode] #{fmt} took #{ms}ms — status=#{status}")
end
end
end
Step 4: 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: FFmpeg disappears silently after OS package upgrades. Without the keyword check, Vigilmon would report HTTP 200 while every transcoding job fails.
Step 5: Heartbeat monitoring for media processing pipelines
If you transcode or process media asynchronously, a heartbeat confirms the pipeline is running:
# lib/my_app/workers/media_pipeline_heartbeat.ex
defmodule MyApp.Workers.MediaPipelineHeartbeat 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_probe()
schedule()
{:noreply, state}
end
defp run_probe do
try do
# Verify Exile can execute a process and return output
output =
Exile.stream!(~w[echo heartbeat-ok])
|> Enum.into([])
|> IO.iodata_to_binary()
|> String.trim()
if output == "heartbeat-ok" do
ping_vigilmon()
else
Logger.error("[MediaPipelineHeartbeat] unexpected probe output: #{output}")
end
rescue
e -> Logger.error("[MediaPipelineHeartbeat] probe failed: #{Exception.message(e)}")
end
end
defp ping_vigilmon do
url = Application.get_env(:my_app, :vigilmon)[:media_heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, %{status: 200}} -> :ok
error -> Logger.warning("Vigilmon heartbeat failed: #{inspect(error)}")
end
end
end
defp schedule, do: Process.send_after(self(), :run, @interval)
end
Add to your supervision tree:
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.Workers.MediaPipelineHeartbeat
]
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval: 10 minutes
- Copy the ping URL
- Set it as
VIGILMON_MEDIA_HEARTBEAT_URLin your environment
Step 6: Monitor for OS process leaks
Exile uses demand-driven I/O to avoid buffering issues, but a downstream consumer that stops reading can cause a process to accumulate. Add a system-level check:
# lib/my_app_web/plugs/health_check.ex (add to run_checks/0)
defp os_process_check do
case System.cmd("pgrep", ["-c", "ffmpeg"], stderr_to_stdout: true) do
{count_str, 0} ->
count = count_str |> String.trim() |> Integer.parse() |> elem(0)
# Alert if more than 10 concurrent ffmpeg processes — likely a leak
status = if count < 10, do: "ok", else: "warning"
%{status: status, ffmpeg_processes: count}
{_, _} ->
# pgrep exits 1 when no processes found — that's fine
%{status: "ok", ffmpeg_processes: 0}
end
end
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 both your HTTP monitor and heartbeat monitor
When FFmpeg goes missing:
🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
4 minutes ago
When the media pipeline stalls:
🔴 HEARTBEAT MISSED: Media Processing Pipeline
Expected ping within 10 minutes — none received
Last seen: 22 minutes ago
Status page:
- Go to Status Pages → New Status Page
- Add your health monitor and heartbeat monitor
- Share the URL with your team
What you've built
| What | How |
|------|-----|
| Health endpoint | Plug running live Exile + FFmpeg probes |
| Process check | pgrep-based ffmpeg count in health response |
| Keyword check | Vigilmon keyword match on "status":"ok" |
| Uptime monitoring | Vigilmon HTTP monitor → /health |
| Pipeline liveness | Heartbeat GenServer + Vigilmon heartbeat monitor |
| Slow processing detection | Telemetry handler with threshold logging |
| Slack alerts | Vigilmon Slack notification channel |
| Status page | Vigilmon public status page |
Exile keeps your media pipelines streaming. Vigilmon tells you when they go silent.
Next steps
- Add per-format heartbeat monitors if you have separate Oban queues for video, audio, and image processing
- Track OS process counts over time to detect gradual ffmpeg leaks before they exhaust system resources
- Use Vigilmon's response time history to correlate media processing latency with queue depth spikes
- Add a Vigilmon keyword check on
"ffmpeg":{"status":"ok"}separately from the top-level status to pinpoint FFmpeg-specific failures
Get started free at vigilmon.online.