tutorial

How to Monitor Vix with Vigilmon

Add uptime monitoring, image-processing health checks, and alerts to your Vix-powered Elixir application — so libvips failures, NIF crashes, and processing pipeline stalls don't silently break your image workflows.

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

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. Enable keyword check: body must contain "status":"ok"
  6. 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:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 10 minutes
  3. Copy the ping URL
  4. Set it as VIGILMON_HEARTBEAT_URL in 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:

  1. In Vigilmon, go to Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. 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:

  1. Go to Status Pages → New Status Page
  2. Add your health monitor and heartbeat monitor
  3. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →