tutorial

How to Monitor eVision with Vigilmon

Add uptime monitoring, computer vision pipeline health checks, and alerts to your eVision-powered Elixir application — so OpenCV failures, model loading errors, and camera feed stalls don't silently break image recognition.

How to Monitor eVision with Vigilmon

eVision provides Elixir bindings for OpenCV, the world's most widely used computer vision library. It enables image detection, face recognition, real-time video processing, and a broad range of machine vision capabilities directly from your Elixir or Phoenix application — without leaving the BEAM.

But eVision depends on native OpenCV binaries compiled against your system libraries. A version mismatch, a missing shared library, or a GPU-accelerated operation falling back to CPU can silently degrade inference latency. External monitoring surfaces what internal supervision trees can't: that the full vision pipeline works end-to-end from the outside.

This tutorial covers:

  • A health endpoint that validates OpenCV availability via eVision
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring to detect when vision processing pipelines stall
  • Slack alerts and a status page

Step 1: Add eVision to your project

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

eVision compiles OpenCV as part of the Hex package. The first mix deps.get && mix deps.compile will take several minutes. Ensure you have the build prerequisites:

# Debian/Ubuntu
apt-get install -y cmake build-essential python3

# After deps.compile, verify eVision loaded
mix run --no-halt -e 'IO.inspect(Evision.getBuildInformation())'

A basic image read and operation:

img = Evision.imread("image.jpg")
gray = Evision.cvtColor(img, Evision.Constant.cv_COLOR_BGR2GRAY())
Evision.imwrite("gray.jpg", gray)

Step 2: Build a health endpoint that validates OpenCV

Add a plug that runs a live eVision probe 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
    evision = evision_check()
    memory = memory_check()

    all_ok = evision.status == "ok" and memory.status == "ok"

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

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

    result =
      try do
        # Create a small test mat, apply a color conversion, verify output
        mat = Evision.Mat.zeros({8, 8, 3}, :u8)
        gray = Evision.cvtColor(mat, Evision.Constant.cv_COLOR_BGR2GRAY())

        elapsed = System.monotonic_time(:millisecond) - start

        if is_struct(gray, Evision.Mat) do
          %{status: "ok", latency_ms: elapsed}
        else
          %{status: "error", reason: "unexpected output type"}
        end
      rescue
        e -> %{status: "error", reason: Exception.message(e)}
      end

    result
  end

  defp memory_check do
    case :memsup.get_system_memory_data() do
      [] ->
        %{status: "ok"}

      data ->
        total = Keyword.get(data, :total_memory, 1)
        free = Keyword.get(data, :free_memory, total)
        used_pct = (total - free) / total * 100
        # OpenCV operations are memory-intensive; warn above 85%
        status = if used_pct < 85, do: "ok", else: "warning"
        %{status: status, used_pct: Float.round(used_pct, 1)}
    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",
#   "evision": {"status": "ok", "latency_ms": 18},
#   "memory": {"status": "ok", "used_pct": 42.3}
# }

Step 3: Track inference metrics with telemetry

Wrap eVision inference calls to emit telemetry on each operation:

# lib/my_app/vision_pipeline.ex
defmodule MyApp.VisionPipeline do
  require Logger

  def detect_faces(image_path) do
    start = System.monotonic_time(:millisecond)

    result =
      try do
        img = Evision.imread(image_path)
        gray = Evision.cvtColor(img, Evision.Constant.cv_COLOR_BGR2GRAY())

        # Load cascade classifier for face detection
        cascade =
          Evision.CascadeClassifier.cascadeClassifier(
            Application.app_dir(:my_app, "priv/haarcascade_frontalface_default.xml")
          )

        faces =
          Evision.CascadeClassifier.detectMultiScale(
            cascade,
            gray,
            scaleFactor: 1.1,
            minNeighbors: 5
          )

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

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

    :telemetry.execute(
      [:my_app, :evision, :detect_faces],
      %{duration_ms: elapsed},
      %{status: status}
    )

    if status == "error" do
      Logger.error("[eVision] face detection 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(
      "evision-detect-logger",
      [:my_app, :evision, :detect_faces],
      &handle_event/4,
      nil
    )
  end

  def handle_event(_event, %{duration_ms: ms}, %{status: status}, _cfg) do
    if ms >= @slow_threshold_ms do
      Logger.warning("[eVision slow inference] #{ms}ms — status=#{status}")
    end
  end
end

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 essential: OpenCV native library loading can fail silently after system updates. Without the check, Vigilmon would report HTTP 200 while every computer vision operation raises a NIF load error.


Step 5: Heartbeat monitoring for vision processing pipelines

If you process images or video frames asynchronously — background jobs running face recognition, object detection, or video analysis — a heartbeat confirms the pipeline is alive:

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

  defp run_probe do
    try do
      # Run a minimal eVision operation to confirm the NIF is functional
      mat = Evision.Mat.zeros({4, 4, 3}, :u8)
      gray = Evision.cvtColor(mat, Evision.Constant.cv_COLOR_BGR2GRAY())

      if is_struct(gray, Evision.Mat) do
        ping_vigilmon()
      else
        Logger.error("[VisionHeartbeat] probe returned unexpected type")
      end
    rescue
      e -> Logger.error("[VisionHeartbeat] probe failed: #{Exception.message(e)}")
    end
  end

  defp ping_vigilmon do
    url = Application.get_env(:my_app, :vigilmon)[:vision_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.VisionPipelineHeartbeat
]

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_VISION_HEARTBEAT_URL in your environment

Step 6: Monitor memory pressure from OpenCV operations

Computer vision operations — especially on high-resolution images or video frames — allocate large native memory buffers outside the BEAM heap. Add memory monitoring to catch pressure before it causes OOM kills:

The health endpoint already includes memory.used_pct. Create a Vigilmon keyword alert on the memory warning state:

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

When memory usage exceeds 85%, the health endpoint returns "warning" — Vigilmon alerts you before the OS starts killing processes.


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 both your HTTP monitor and heartbeat monitor

When OpenCV fails to initialize:

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

When the vision pipeline stalls:

🔴 HEARTBEAT MISSED: Vision Processing Pipeline
Expected ping within 10 minutes — none received
Last seen: 19 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 eVision OpenCV probe | | Memory check | :memsup-based memory pressure monitoring | | Keyword check | Vigilmon keyword match on "status":"ok" | | Uptime monitoring | Vigilmon HTTP monitor → /health | | Pipeline liveness | Heartbeat GenServer + Vigilmon heartbeat monitor | | Slow inference detection | Telemetry handler with threshold logging | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

eVision gives your Elixir apps computer vision. Vigilmon tells you when the eyes go dark.


Next steps

  • Add per-operation heartbeat monitors if you have separate queues for face detection, object detection, and video frame processing
  • Track inference latency percentiles over time to detect model performance regressions without code changes
  • Use Vigilmon's response time history to correlate eVision latency with upstream image resolution changes
  • Add a dedicated memory monitor with a tighter threshold for GPU-accelerated deployments where VRAM exhaustion is a separate failure mode

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 →