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
- 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 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:
- Click New Monitor → Heartbeat
- Set expected interval: 10 minutes
- Copy the ping URL
- Set it as
VIGILMON_HEARTBEAT_URLin 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:
- In Vigilmon, click New Monitor → HTTP
- URL:
https://yourdomain.com/health - Enable keyword check: body must NOT contain
"disk":{"status":"warning" - 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:
- 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 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:
- 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 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.