How to Monitor Zigler with Vigilmon
Zigler lets you write Elixir Native Implemented Functions (NIFs) in Zig — a systems language that gives you C-level performance with compile-time memory safety guarantees. When you need to run compute-intensive work directly on the BEAM scheduler thread (or in a dirty scheduler), Zigler is the idiomatic choice: no c_src/ boilerplate, no manual NIF registration macros, just Zig functions annotated with beam types.
The risk is proportional to the power. A NIF that blocks the scheduler longer than 1 ms degrades every process on the node. A Zig function that panics or violates memory safety brings down the entire BEAM VM — not just the calling process. External monitoring is the only way to know when a NIF regression slips past your test suite and into production.
This tutorial covers:
- A health check endpoint that exposes NIF execution statistics
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for dirty-scheduler NIF jobs
- Alerts when NIF latency or scheduler blockage exceeds safe thresholds
Why Monitor Zigler?
Zig NIFs fail in ways that don't appear in BEAM crash reports:
| Failure mode | Symptom without monitoring |
|---|---|
| NIF blocks scheduler > 1 ms | BEAM responsiveness degrades; no crash |
| Zig panic in non-dirty NIF | Entire VM crashes; no Erlang stack trace |
| Memory allocation failure in Zig | enomem returned; callers receive opaque error |
| Dirty NIF thread pool exhaustion | Callers queue silently; latency spikes |
| Zig segfault via unsafe pointer cast | VM segfault; OS-level core dump only |
| NIF version mismatch after hot reload | badarg errors on every call |
Step 1: Telemetry wrapper around Zigler NIFs
Zigler-generated NIFs are called like regular Elixir functions. Wrap them with :telemetry to track invocation counts and latency:
# lib/my_app/nif_wrapper.ex
defmodule MyApp.NifWrapper do
require Logger
@doc """
Wraps a Zigler NIF call with telemetry events for latency and error tracking.
"""
def call(module, function, args) do
start_time = System.monotonic_time()
:telemetry.execute([:my_app, :nif, :start], %{}, %{module: module, function: function})
try do
result = apply(module, function, args)
duration = System.monotonic_time() - start_time
:telemetry.execute(
[:my_app, :nif, :stop],
%{duration: duration},
%{module: module, function: function}
)
result
rescue
error ->
:telemetry.execute(
[:my_app, :nif, :exception],
%{},
%{module: module, function: function, error: error}
)
reraise error, __STACKTRACE__
end
end
end
Attach a handler that accumulates stats in a GenServer:
# lib/my_app/nif_monitor.ex
defmodule MyApp.NifMonitor do
use GenServer
@slow_threshold_ms 5
def start_link(_), do: GenServer.start_link(__MODULE__, initial_state(), name: __MODULE__)
def stats, do: GenServer.call(__MODULE__, :stats)
defp initial_state do
%{total_calls: 0, errors: 0, slow_calls: 0, total_duration_ms: 0}
end
@impl true
def init(state) do
:ok = :telemetry.attach_many(
"nif-monitor",
[[:my_app, :nif, :stop], [:my_app, :nif, :exception]],
&__MODULE__.handle_event/4,
nil
)
{:ok, state}
end
def handle_event([:my_app, :nif, :stop], %{duration: duration}, _meta, _config) do
duration_ms = System.convert_time_unit(duration, :native, :millisecond)
GenServer.cast(__MODULE__, {:call_completed, duration_ms})
end
def handle_event([:my_app, :nif, :exception], _measurements, _meta, _config) do
GenServer.cast(__MODULE__, :call_failed)
end
@impl true
def handle_cast({:call_completed, duration_ms}, state) do
slow = if duration_ms > @slow_threshold_ms, do: 1, else: 0
{:noreply, %{state |
total_calls: state.total_calls + 1,
slow_calls: state.slow_calls + slow,
total_duration_ms: state.total_duration_ms + duration_ms
}}
end
def handle_cast(:call_failed, state) do
{:noreply, %{state | errors: state.errors + 1}}
end
@impl true
def handle_call(:stats, _from, state) do
avg_ms = if state.total_calls > 0,
do: Float.round(state.total_duration_ms / state.total_calls, 2),
else: 0.0
{:reply, Map.put(state, :avg_duration_ms, avg_ms), state}
end
end
Add to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.NifMonitor,
]
Step 2: Health check endpoint
# 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
nif_stats = MyApp.NifMonitor.stats()
db_ok = check_database()
nif_ok =
nif_stats.errors == 0 and
nif_stats.slow_calls < 10 and
nif_stats.avg_duration_ms < 50.0
status = if db_ok and nif_ok, do: 200, else: 503
body = Jason.encode!(%{
status: if(status == 200, do: "ok", else: "degraded"),
checks: %{
database: if(db_ok, do: "ok", else: "error"),
nif_health: if(nif_ok, do: "ok", else: "degraded"),
nif_stats: nif_stats
}
})
conn
|> put_resp_content_type("application/json")
|> send_resp(status, body)
|> halt()
end
def call(conn, _opts), do: conn
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> true
_ -> false
end
end
end
Wire it in before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
curl http://localhost:4000/health
# {"status":"ok","checks":{"database":"ok","nif_health":"ok","nif_stats":{...}}}
Step 3: Set up HTTP monitoring in Vigilmon
- Log in at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set interval: 1 minute
- Add a keyword check for
"degraded"— set it to alert when the keyword IS present - Set threshold: 2 consecutive failures before alerting
The keyword check catches cases where a NIF regression causes nif_health to read "degraded" even if the HTTP status is still 200 (for example, if you soft-degrade instead of returning 503).
Step 4: Heartbeat monitoring for long-running dirty NIFs
Zigler NIFs running on dirty schedulers are the safe way to do work over 1 ms. But a dirty NIF that silently hangs blocks a dirty scheduler thread and queues subsequent callers. Add a heartbeat to your wrapper:
# lib/my_app/heavy_computation.ex
defmodule MyApp.HeavyComputation do
use Zigler
@heartbeat_url System.get_env("VIGILMON_NIF_HEARTBEAT_URL")
# Zigler NIF declaration (compiled from Zig source)
~Z"""
pub fn compute_hash(env: beam.env, data: []const u8) beam.term {
// Zig implementation here
return beam.make(env, @as(u64, 0), .{});
}
"""
def run_with_heartbeat(data) do
result = MyApp.NifWrapper.call(__MODULE__, :compute_hash, [data])
ping_heartbeat()
result
end
defp ping_heartbeat do
if @heartbeat_url do
Task.start(fn ->
:httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 5000}], [])
end)
end
end
end
Create the heartbeat monitor in Vigilmon:
- Go to Monitors → New → Heartbeat
- Set grace period to the expected NIF call period + 50% buffer (e.g. if called every 60 s, set 90 s)
- Copy the ping URL into
VIGILMON_NIF_HEARTBEAT_URL
If the dirty scheduler thread deadlocks, the heartbeat stops and Vigilmon pages you before your users notice.
Step 5: Key metrics to alert on
| Metric | Alert threshold | Why |
|---|---|---|
| /health HTTP status | Non-200 for 2 checks | Full application down |
| nif_health keyword "degraded" | Any match | NIF error or latency regression |
| avg_duration_ms in response | > 50 ms | Scheduler blocking risk |
| slow_calls in response | > 10 | Repeated latency spikes |
| Dirty NIF heartbeat silence | Grace period exceeded | Scheduler thread hung |
Step 6: Status page
- In Vigilmon, go to Status Pages → New
- Add your HTTP health monitor and dirty NIF heartbeat
- Label them: "API (Web)" and "NIF Compute Layer"
- Share the URL with your engineering team
Conclusion
Zigler makes writing high-performance Zig NIFs from Elixir approachable, but NIFs operate below the BEAM supervision tree — a crash takes the whole VM down. With Vigilmon you get:
- HTTP uptime checks that detect BEAM crashes and NIF degradation in your health endpoint
- Keyword alerts that surface slow or error-prone NIFs before they cause outages
- Heartbeat monitors that catch dirty scheduler thread deadlocks
Instrument NIF calls with telemetry first, then add monitoring as your Zig NIF surface area grows.
Get started free at vigilmon.online.