How to Monitor Thumbnex with Vigilmon
Thumbnex is an Elixir library for generating image thumbnails from a wide variety of file types — including PDFs, Office documents (DOCX, XLSX, PPTX), and images — by delegating to LibreOffice and ImageMagick under the hood. It's the most practical way to generate document previews in a Phoenix application without reinventing file conversion.
But Thumbnex is only as reliable as its external dependencies: a missing LibreOffice binary, an ImageMagick version mismatch, or a locked temporary directory will silently break every document preview in your app. External monitoring catches what internal error handling can't — that the whole pipeline is reachable and working from the outside.
This tutorial covers:
- A health endpoint that validates LibreOffice and ImageMagick availability
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring to detect when thumbnail generation pipelines stall
- Slack alerts and a status page
Step 1: Add Thumbnex to your project
# mix.exs
defp deps do
[
{:thumbnex, "~> 0.4"},
{:req, "~> 0.4"} # for heartbeat pings
]
end
Thumbnex requires both LibreOffice (for Office/PDF conversion) and ImageMagick (for resizing and image output). Install both on your server:
# Debian/Ubuntu
apt-get install -y libreoffice imagemagick ghostscript
# Verify
libreoffice --version
# LibreOffice 7.x.x ...
convert --version
# Version: ImageMagick 7.x ...
A basic thumbnail:
{:ok, path} = Thumbnex.create_thumbnail("document.docx", "thumbnail.png", max_width: 800, max_height: 600)
Step 2: Build a health endpoint that validates the pipeline
Add a plug that runs a real thumbnail generation 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
thumbnex = thumbnex_check()
disk = disk_check()
all_ok = thumbnex.status == "ok" and disk.status == "ok"
%{
status: if(all_ok, do: "ok", else: "degraded"),
thumbnex: thumbnex,
disk: disk
}
end
defp thumbnex_check do
start = System.monotonic_time(:millisecond)
tmp_dir = System.tmp_dir!()
tmp_in = Path.join(tmp_dir, "hc_in_#{System.unique_integer()}.png")
tmp_out = Path.join(tmp_dir, "hc_out_#{System.unique_integer()}.png")
result =
try do
# Create a minimal test image to thumbnail
case System.cmd("convert", ["-size", "10x10", "xc:white", tmp_in], stderr_to_stdout: true) do
{_, 0} ->
case Thumbnex.create_thumbnail(tmp_in, tmp_out, max_width: 5, max_height: 5) do
{:ok, _} ->
elapsed = System.monotonic_time(:millisecond) - start
%{status: "ok", latency_ms: elapsed}
{:error, reason} ->
%{status: "error", reason: inspect(reason)}
end
{output, code} ->
%{status: "error", reason: "imagemagick exited #{code}: #{String.slice(output, 0, 200)}"}
end
rescue
e -> %{status: "error", reason: Exception.message(e)}
after
File.rm(tmp_in)
File.rm(tmp_out)
end
result
end
defp disk_check do
tmp = System.tmp_dir!()
case System.cmd("df", ["-k", tmp], stderr_to_stdout: true) do
{output, 0} ->
case String.split(output, "\n", trim: true) do
[_header, data | _] ->
parts = String.split(data, ~r/\s+/)
available_kb = parts |> Enum.at(3, "0") |> Integer.parse() |> elem(0)
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 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",
# "thumbnex": {"status": "ok", "latency_ms": 320},
# "disk": {"status": "ok", "available_mb": 8192}
# }
A 503 response with "status": "degraded" tells you exactly which dependency failed.
Step 3: Track thumbnail generation metrics with telemetry
Wrap Thumbnex calls to emit telemetry for slow or failed conversions:
# lib/my_app/document_thumbnailer.ex
defmodule MyApp.DocumentThumbnailer do
require Logger
def generate(source_path, output_path, opts \\ []) do
start = System.monotonic_time(:millisecond)
result = Thumbnex.create_thumbnail(source_path, output_path, opts)
elapsed = System.monotonic_time(:millisecond) - start
ext = Path.extname(source_path)
status = if match?({:ok, _}, result), do: "ok", else: "error"
:telemetry.execute(
[:my_app, :thumbnex, :generate],
%{duration_ms: elapsed},
%{extension: ext, status: status}
)
if status == "error" do
Logger.error("[Thumbnex] generation failed in #{elapsed}ms for #{ext}: #{inspect(result)}")
end
result
end
end
Attach a telemetry handler to log slow conversions:
# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
require Logger
@slow_threshold_ms 10_000
def setup do
:telemetry.attach(
"thumbnex-generate-logger",
[:my_app, :thumbnex, :generate],
&handle_event/4,
nil
)
end
def handle_event(_event, %{duration_ms: ms}, %{extension: ext, status: status}, _cfg) do
if ms >= @slow_threshold_ms do
Logger.warning("[Thumbnex slow] #{ext} took #{ms}ms — status=#{status}")
end
end
end
Call MyApp.Telemetry.setup() from 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 essential: LibreOffice can disappear after a system update while the endpoint continues returning HTTP 200. Without the check, every document preview silently fails while Vigilmon reports green.
Step 5: Heartbeat monitoring for thumbnail generation queues
If you process thumbnails asynchronously — background jobs generating previews for uploaded files — a heartbeat confirms the pipeline is alive:
# lib/my_app/workers/thumbnailer_heartbeat.ex
defmodule MyApp.Workers.ThumbnailerHeartbeat do
use GenServer
require Logger
@interval :timer.minutes(10)
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
tmp_dir = System.tmp_dir!()
tmp_in = Path.join(tmp_dir, "hb_in_#{System.unique_integer()}.png")
tmp_out = Path.join(tmp_dir, "hb_out_#{System.unique_integer()}.png")
try do
{_, 0} = System.cmd("convert", ["-size", "4x4", "xc:white", tmp_in])
case MyApp.DocumentThumbnailer.generate(tmp_in, tmp_out, max_width: 2, max_height: 2) do
{:ok, _} ->
ping_vigilmon()
{:error, reason} ->
Logger.error("[ThumbnailerHeartbeat] probe failed: #{inspect(reason)}")
end
rescue
e -> Logger.error("[ThumbnailerHeartbeat] error: #{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)[:thumbnailer_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.ThumbnailerHeartbeat
]
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval: 15 minutes
- Copy the ping URL
- Set it as
VIGILMON_THUMBNAILER_HEARTBEAT_URLin your environment
Step 6: 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 LibreOffice breaks or disappears:
🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West, US-East
3 minutes ago
When the thumbnail pipeline stalls:
🔴 HEARTBEAT MISSED: Thumbnailer Pipeline
Expected ping within 15 minutes — none received
Last seen: 28 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 Thumbnex probe |
| Disk check | df-based available-space check |
| Keyword check | Vigilmon keyword match on "status":"ok" |
| Uptime monitoring | Vigilmon HTTP monitor → /health |
| Pipeline liveness | Heartbeat GenServer + Vigilmon heartbeat monitor |
| Slow conversion detection | Telemetry handler with threshold logging |
| Slack alerts | Vigilmon Slack notification channel |
| Status page | Vigilmon public status page |
Thumbnex keeps your document previews sharp. Vigilmon tells you when the pipeline goes dark.
Next steps
- Track per-format (PDF, DOCX, XLSX) conversion latency in the health response to detect format-specific regressions
- Add a LibreOffice-specific probe that converts a tiny test PDF to verify Office conversion separately from image-to-image
- Use Vigilmon's response time history to correlate thumbnail latency with upload volume spikes
- Add separate heartbeat monitors per Oban queue if you process different document types in separate queues
Get started free at vigilmon.online.