How to Monitor Vix with Vigilmon
Vix is a high-performance Elixir image processing library built on libvips. It processes images at a fraction of the memory and CPU cost of ImageMagick by streaming pixel data rather than loading entire images into memory. But Vix operates through NIFs (Native Implemented Functions) — if the libvips shared library is missing, the wrong version is loaded, or a NIF segfaults, your entire Erlang VM can crash rather than returning a clean error.
External monitoring surfaces what Vix can't self-report. This tutorial covers:
- A health endpoint that validates libvips availability and a test transformation
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring to detect when image processing pipelines stall
- Slack alerts and a status page
Step 1: Add Vix to your project
# mix.exs
defp deps do
[
{:vix, "~> 0.26"},
{:req, "~> 0.4"} # for heartbeat pings
]
end
libvips must be installed on your server:
# Debian/Ubuntu
apt-get install -y libvips libvips-dev
# Verify
vips --version
# vips-8.x.y ...
A basic resize with Vix:
alias Vix.Vips.Image
alias Vix.Vips.Operation
{:ok, img} = Image.new_from_file("input.jpg")
{:ok, resized} = Operation.thumbnail_image(img, 800)
:ok = Image.write_to_file(resized, "output.jpg")
Step 2: Build a health endpoint that validates libvips
Add a plug that exercises a real Vix transformation 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
vix = vix_check()
memory = memory_check()
all_ok = vix.status == "ok" and memory.status in ["ok", "unknown"]
%{
status: if(all_ok, do: "ok", else: "degraded"),
vix: vix,
memory: memory
}
end
defp vix_check do
start = System.monotonic_time(:millisecond)
result =
try do
alias Vix.Vips.Image
alias Vix.Vips.Operation
# Create a 1x1 pixel black image entirely in memory — no disk I/O
{:ok, img} = Image.new_from_list([[0]], bands: 1)
{:ok, _resized} = Operation.resize(img, 1.0)
elapsed = System.monotonic_time(:millisecond) - start
%{status: "ok", latency_ms: elapsed, libvips_version: Vix.Vips.version()}
rescue
e -> %{status: "error", reason: Exception.message(e)}
end
result
end
defp memory_check do
# Vix exposes memory stats via libvips
try do
stats = Vix.Vips.cache_stats()
%{status: "ok", vips_cache: stats}
rescue
_ -> %{status: "unknown"}
end
end
end
Register the plug:
# 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",
# "vix": {"status": "ok", "latency_ms": 12, "libvips_version": "8.15.1"},
# "memory": {"status": "ok", "vips_cache": {...}}
# }
A 503 with "status": "degraded" means libvips is either missing or crashed during the probe.
Step 3: Track image processing metrics with telemetry
Wrap your Vix processing calls to emit telemetry events and handle errors gracefully:
# lib/my_app/image_processor.ex
defmodule MyApp.ImageProcessor do
require Logger
alias Vix.Vips.Image
alias Vix.Vips.Operation
def resize(source_path, output_path, width) do
start = System.monotonic_time(:millisecond)
result =
with {:ok, img} <- Image.new_from_file(source_path),
{:ok, resized} <- Operation.thumbnail_image(img, width),
:ok <- Image.write_to_file(resized, output_path) do
:ok
else
{:error, reason} -> {:error, inspect(reason)}
end
elapsed = System.monotonic_time(:millisecond) - start
status = if result == :ok, do: "ok", else: "error"
:telemetry.execute(
[:my_app, :vix, :resize],
%{duration_ms: elapsed},
%{width: width, status: status}
)
if result != :ok do
Logger.error("[Vix] resize to #{width}px failed in #{elapsed}ms: #{inspect(result)}")
end
result
end
end
Attach a telemetry handler:
# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
require Logger
@slow_threshold_ms 3_000
def setup do
:telemetry.attach(
"vix-resize-logger",
[:my_app, :vix, :resize],
&handle_event/4,
nil
)
end
def handle_event(_event, %{duration_ms: ms}, %{width: width, status: status}, _cfg) do
if ms >= @slow_threshold_ms do
Logger.warning("[Vix slow resize] #{width}px took #{ms}ms — status=#{status}")
end
end
end
Call MyApp.Telemetry.setup() in application.ex.
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 with Vix: a NIF version mismatch between the compiled Vix package and the installed libvips will raise on the first call — but your web server process will still be running. Without the keyword check, Vigilmon would see HTTP 200 while every image operation silently errors.
Step 5: Heartbeat monitoring for image processing pipelines
Vix excels in high-throughput pipelines — batch thumbnail generation, format conversion queues, video frame extraction. Monitor those pipelines with a heartbeat:
# lib/my_app/workers/vix_pipeline_worker.ex
defmodule MyApp.Workers.VixPipelineWorker do
use GenServer
require Logger
alias Vix.Vips.Image
alias Vix.Vips.Operation
@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
try do
{:ok, img} = Image.new_from_list([[255, 255, 255], [255, 255, 255]], bands: 3)
{:ok, _resized} = Operation.resize(img, 0.5)
ping_vigilmon()
rescue
e -> Logger.error("[Vix heartbeat] check failed: #{Exception.message(e)}")
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 to your supervision tree:
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.Workers.VixPipelineWorker
]
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 the NIF crashes, libvips is replaced by a package manager upgrade, or the pipeline GenServer exits without restarting, the heartbeat stops — Vigilmon alerts you within one missed interval.
Step 6: Expose libvips version in the health response
Library version mismatches are a common source of silent failures with NIFs. Expose the loaded libvips version in your health response so you can detect unexpected upgrades:
# Add to vix_check/0
%{
status: "ok",
latency_ms: elapsed,
libvips_version: Vix.Vips.version(),
vix_version: Application.spec(:vix, :vsn) |> to_string()
}
Set a Vigilmon alert if the health body changes: store the expected version string and alert if the keyword disappears. A silent libvips upgrade in a container rebuild that changes behavior but not HTTP status will now surface as an anomaly.
Step 7: Monitor memory usage — Vix's key advantage
Vix's primary benefit over Mogrify is memory efficiency. Include memory stats in the health endpoint to verify that benefit is being realized:
# Add to run_checks/0
defp erlang_memory_check do
memory = :erlang.memory()
total_mb = div(memory[:total], 1_048_576)
process_mb = div(memory[:processes], 1_048_576)
status = if total_mb < 1_024, do: "ok", else: "warning"
%{
status: status,
total_mb: total_mb,
process_mb: process_mb
}
end
If memory trends up despite Vix's streaming design, it may indicate that large image buffers are being held in process state rather than released. Vigilmon's response time history lets you correlate memory growth with processing volume.
Step 8: 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 health monitor and heartbeat monitor
When libvips breaks:
🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
2 minutes ago
When the image pipeline stalls:
🔴 HEARTBEAT MISSED: Vix Image Pipeline
Expected ping within 10 minutes — none received
Last seen: 13 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 an in-memory Vix transformation |
| libvips version check | Version string in health response for upgrade detection |
| Memory monitoring | Erlang memory stats 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 |
Vix keeps your image processing fast and memory-efficient. Vigilmon tells you when the NIF layer stops cooperating.
Next steps
- Track throughput (images/minute) in the health response to detect pipeline slowdowns before they queue up
- Alert on libvips version changes using Vigilmon's keyword match to catch unexpected container rebuilds
- Use Vigilmon's response time history to compare Vix processing latency before and after infrastructure changes
Get started free at vigilmon.online.