tutorial

How to Monitor Mogrify with Vigilmon

Add uptime monitoring, image-processing pipeline health checks, and alerts to your Mogrify-powered Elixir application — so ImageMagick failures, disk pressure, and processing bottlenecks don't silently break uploads.

How to Monitor Mogrify with Vigilmon

Mogrify is the go-to Elixir library for image processing. It wraps ImageMagick to provide resizing, cropping, format conversion, and filter application through a clean Elixir API. But Mogrify is only as reliable as the ImageMagick binary beneath it: a missing convert executable, an OOM-killed process during a large resize, or a disk-full condition on the temp directory will silently break every image upload in your Phoenix application.

External monitoring surfaces what Mogrify can't self-report. This tutorial covers:

  • A health endpoint that validates ImageMagick availability and a test image transformation
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring to detect when image processing pipelines stall
  • Slack alerts and a status page

Step 1: Add Mogrify to your project

# mix.exs
defp deps do
  [
    {:mogrify, "~> 0.9"},
    {:req, "~> 0.4"}   # for heartbeat pings
  ]
end

ImageMagick must be installed on your server:

# Debian/Ubuntu
apt-get install -y imagemagick

# Verify
convert --version
# Version: ImageMagick 7.x ...

A basic resize:

import Mogrify

open("input.jpg")
|> resize("800x600")
|> save(path: "output.jpg")

Step 2: Build a health endpoint that validates ImageMagick

Add a plug that runs a test image 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
    imagemagick = imagemagick_check()
    disk = disk_check()

    all_ok = imagemagick.status == "ok" and disk.status == "ok"

    %{
      status: if(all_ok, do: "ok", else: "degraded"),
      imagemagick: imagemagick,
      disk: disk
    }
  end

  defp imagemagick_check do
    start = System.monotonic_time(:millisecond)

    result =
      try do
        # Create a tiny test image in memory and apply a no-op transformation
        tmp_in = Path.join(System.tmp_dir!(), "health_probe_in_#{System.unique_integer()}.png")
        tmp_out = Path.join(System.tmp_dir!(), "health_probe_out_#{System.unique_integer()}.png")

        # Generate a 1x1 white PNG using ImageMagick
        case System.cmd("convert", ["-size", "1x1", "xc:white", tmp_in], stderr_to_stdout: true) do
          {_, 0} ->
            import Mogrify

            open(tmp_in)
            |> resize("1x1")
            |> save(path: tmp_out)

            File.rm(tmp_in)
            File.rm(tmp_out)

            elapsed = System.monotonic_time(:millisecond) - start
            %{status: "ok", latency_ms: elapsed}

          {output, code} ->
            %{status: "error", reason: "convert exited #{code}: #{output}"}
        end
      rescue
        e -> %{status: "error", reason: Exception.message(e)}
      end

    result
  end

  defp disk_check do
    tmp = System.tmp_dir!()

    case System.cmd("df", ["-k", tmp], stderr_to_stdout: true) do
      {output, 0} ->
        lines = String.split(output, "\n", trim: true)
        # Parse the data line (second line of df output)
        case lines do
          [_header, data | _] ->
            parts = String.split(data, ~r/\s+/)
            available_kb = parts |> Enum.at(3, "0") |> Integer.parse() |> elem(0)
            # Warn if less than 500 MB free
            status = if available_kb > 512_000, do: "ok", else: "warning"
            %{status: status, available_mb: div(available_kb, 1024)}

          _ ->
            %{status: "unknown"}
        end

      {_, _} ->
        %{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",
#   "imagemagick": {"status": "ok", "latency_ms": 45},
#   "disk": {"status": "ok", "available_mb": 12048}
# }

A 503 with "status": "degraded" means either ImageMagick is broken or disk space is critically low.


Step 3: Track image processing metrics with telemetry

Wrap your image processing calls to emit telemetry events:

# lib/my_app/image_processor.ex
defmodule MyApp.ImageProcessor do
  require Logger

  def resize(source_path, output_path, dimensions) do
    start = System.monotonic_time(:millisecond)

    result =
      try do
        import Mogrify

        open(source_path)
        |> resize(dimensions)
        |> save(path: output_path)

        :ok
      rescue
        e ->
          {:error, Exception.message(e)}
      end

    elapsed = System.monotonic_time(:millisecond) - start
    status = if result == :ok, do: "ok", else: "error"

    :telemetry.execute(
      [:my_app, :mogrify, :resize],
      %{duration_ms: elapsed},
      %{dimensions: dimensions, status: status}
    )

    if result != :ok do
      Logger.error("[Mogrify] resize 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 5_000

  def setup do
    :telemetry.attach(
      "mogrify-resize-logger",
      [:my_app, :mogrify, :resize],
      &handle_event/4,
      nil
    )
  end

  def handle_event(_event, %{duration_ms: ms}, %{dimensions: dims, status: status}, _cfg) do
    if ms >= @slow_threshold_ms do
      Logger.warning("[Mogrify slow resize] #{dims} 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 matters: ImageMagick can disappear after a system update or a bad package upgrade. Without the check, Vigilmon would see HTTP 200 even as every image upload silently fails.


Step 5: Heartbeat monitoring for image processing pipelines

If you process images asynchronously — background resizing, thumbnail generation, format conversion queues — a heartbeat confirms the pipeline is running:

# lib/my_app/workers/image_pipeline_worker.ex
defmodule MyApp.Workers.ImagePipelineWorker 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_check()
    schedule()
    {:noreply, state}
  end

  defp run_check do
    tmp_in = Path.join(System.tmp_dir!(), "pipeline_hb_#{System.unique_integer()}.png")
    tmp_out = Path.join(System.tmp_dir!(), "pipeline_hb_out_#{System.unique_integer()}.png")

    try do
      {_, 0} = System.cmd("convert", ["-size", "4x4", "xc:white", tmp_in])

      case MyApp.ImageProcessor.resize(tmp_in, tmp_out, "2x2") do
        :ok ->
          ping_vigilmon()

        {:error, reason} ->
          Logger.error("[ImagePipeline heartbeat] resize failed: #{reason}")
      end
    rescue
      e -> Logger.error("[ImagePipeline heartbeat] check failed: #{Exception.message(e)}")
    after
      File.rm(tmp_in)
      File.rm(tmp_out)
    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.ImagePipelineWorker
]

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

Step 6: Monitor disk usage trends

Image processing is disk-hungry. Add a Vigilmon monitor on the disk check to catch pressure before it becomes an outage:

The health endpoint already includes disk.available_mb. Set a Vigilmon keyword alert to fire on "status":"warning" in the disk section:

  1. In Vigilmon, click New Monitor → HTTP
  2. URL: https://yourdomain.com/health
  3. Enable keyword check: body must NOT contain "disk":{"status":"warning"
  4. Save

When available disk drops below 500 MB, the health endpoint returns "warning" — Vigilmon alerts you before writes start failing.


Step 7: 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 ImageMagick breaks:

🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
2 minutes ago

When the image pipeline stalls:

🔴 HEARTBEAT MISSED: Image Processing Pipeline
Expected ping within 10 minutes — none received
Last seen: 17 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 a live ImageMagick test transformation | | Disk check | df-based available-space check 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 |

Mogrify keeps your images sharp. Vigilmon tells you when the pipeline goes dark.


Next steps

  • Track processed image count and average resize time in the health response to detect throughput regressions
  • Add disk-full alerts using Vigilmon's keyword negation check on the "warning" state
  • Use Vigilmon's response time history to correlate image processing latency with upload volume spikes

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 →