How to Monitor Matcha with Vigilmon
Matcha is a compile-time match specification DSL for Elixir. It compiles readable filter patterns into valid Erlang MatchSpec structures at build time, giving you type-safe, composable queries for ETS tables, Mnesia databases, and tracing calls — without constructing match specs by hand or discovering malformed specs at runtime.
When Matcha powers your ETS-backed cache, session store, or rate-limiter, those tables become load-bearing infrastructure. A table that fills, corrupts, or loses its maintenance worker silently degrades your app — Matcha's compile-time safety does not protect you from runtime capacity or availability issues. You need external monitoring to catch those.
This tutorial covers:
- A health endpoint that reports ETS table sizes and memory usage
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for background ETS maintenance workers (TTL expiry, compaction)
- Alerts calibrated to table-size and memory thresholds
Why Monitor Matcha?
| Failure mode | Symptom without monitoring | |---|---| | ETS table grows unbounded | VM memory climbs until OOM kill | | TTL expiry worker crashes | Stale entries accumulate; cache never evicts | | Table not found (application crash at startup) | All Matcha queries fail; app returns 500 | | Match specification generates full table scan | Latency spike on every lookup | | Mnesia cluster split-brain | Queries return stale or inconsistent data |
Step 1: Health endpoint with ETS table stats
Add Matcha to your dependencies:
# mix.exs
{:matcha, "~> 0.1"}
Create a module that tracks your critical ETS tables and exposes health data:
# lib/my_app/ets_monitor.ex
defmodule MyApp.EtsMonitor do
@monitored_tables [:sessions, :rate_limits, :feature_flags]
@max_table_size 500_000
@max_table_mb 256
def health do
checks =
Enum.map(@monitored_tables, fn table ->
{table, table_health(table)}
end)
|> Map.new()
stats =
Enum.map(@monitored_tables, fn table ->
{table, table_stats(table)}
end)
|> Map.new()
all_ok = Enum.all?(checks, fn {_, v} -> v == :ok end)
{all_ok, checks, stats}
end
defp table_health(table) do
case table_stats(table) do
%{exists: false} -> :error
%{size: size} when size > @max_table_size -> :error
%{memory_mb: mb} when mb > @max_table_mb -> :error
_ -> :ok
end
end
defp table_stats(table) do
case :ets.info(table) do
:undefined ->
%{exists: false}
info ->
words = Keyword.get(info, :memory, 0)
%{
exists: true,
size: Keyword.get(info, :size, 0),
memory_mb: Float.round(words * :erlang.system_info(:wordsize) / 1_048_576, 2)
}
end
end
end
Wire it into your health plug:
# 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
{ets_ok, ets_checks, ets_stats} = MyApp.EtsMonitor.health()
db_ok = check_database()
all_ok = db_ok and ets_ok
status = if all_ok, do: 200, else: 503
body = Jason.encode!(%{
status: if(all_ok, do: "ok", else: "degraded"),
checks: Map.merge(%{database: if(db_ok, do: "ok", else: "error")}, ets_checks),
ets: ets_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
Test it:
curl http://localhost:4000/health | jq .
# {
# "status": "ok",
# "checks": {"database": "ok", "sessions": "ok", "rate_limits": "ok"},
# "ets": {
# "sessions": {"exists": true, "size": 1234, "memory_mb": 0.8},
# "rate_limits": {"exists": true, "size": 892, "memory_mb": 0.3}
# }
# }
Step 2: A Matcha-powered example — session lookup
To illustrate what you are monitoring, here is a typical Matcha query against the :sessions table:
# lib/my_app/session_cache.ex
defmodule MyApp.SessionCache do
require Matcha
def lookup_active(user_id) do
:ets.select(:sessions, Matcha.spec do
{^user_id, session_id, expires_at, data}
when expires_at > ^System.system_time(:second) ->
{session_id, data}
end)
end
def put(user_id, session_id, data, ttl_seconds) do
expires_at = System.system_time(:second) + ttl_seconds
:ets.insert(:sessions, {user_id, session_id, expires_at, data})
end
end
The compile-time match spec means a malformed spec raises at compile time, not in production. But it does not protect against the table being full, missing, or corrupted — which is exactly what the health endpoint above covers.
Step 3: Set up HTTP monitoring in Vigilmon
- Sign up at vigilmon.online.
- Click New Monitor → HTTP.
- Enter
https://your-app.example.com/health. - Set check interval to 60 seconds.
- Add a keyword check for
"ok"to catch degraded responses with HTTP 200. - Set alert threshold to 2 consecutive failures.
Step 4: Heartbeat monitoring for ETS maintenance workers
ETS tables backing session caches and rate-limiters need periodic TTL sweeps. Matcha makes those sweeps readable; Vigilmon makes sure they keep running.
# lib/my_app/session_sweeper.ex
defmodule MyApp.SessionSweeper do
use GenServer
require Logger
require Matcha
@interval :timer.minutes(5)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_sweep()
{:ok, state}
end
@impl true
def handle_info(:sweep, state) do
case run_sweep() do
{:ok, deleted} ->
Logger.info("SessionSweeper removed #{deleted} expired entries")
ping_heartbeat()
{:error, reason} ->
Logger.error("SessionSweeper failed: #{inspect(reason)}")
end
schedule_sweep()
{:noreply, state}
end
defp run_sweep do
now = System.system_time(:second)
expired =
:ets.select(:sessions, Matcha.spec do
{user_id, session_id, expires_at, _data}
when expires_at < ^now ->
{user_id, session_id}
end)
Enum.each(expired, fn {user_id, session_id} ->
:ets.match_delete(:sessions, {user_id, session_id, :_, :_})
end)
{:ok, length(expired)}
rescue
e -> {:error, e}
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:sweeper_heartbeat_url]
if url do
Task.start(fn ->
:httpc.request(:get, {String.to_charlist(url), []}, [], [])
end)
end
end
defp schedule_sweep, do: Process.send_after(self(), :sweep, @interval)
end
Add to your supervision tree:
children = [
MyApp.Repo,
{MyApp.SessionCache, []}, # creates the :sessions ETS table
MyApp.SessionSweeper,
MyAppWeb.Endpoint,
]
Configure the heartbeat URL:
# config/runtime.exs
config :my_app, :vigilmon,
sweeper_heartbeat_url: System.get_env("VIGILMON_SWEEPER_HEARTBEAT_URL")
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat.
- Grace period: 10 minutes (5-min sweep interval + buffer).
- Copy the ping URL into
VIGILMON_SWEEPER_HEARTBEAT_URL.
Step 5: Key metrics to alert on
| Metric | Alert threshold | Why |
|---|---|---|
| /health HTTP status | Non-200 for 2 checks | App or database unreachable |
| ETS table size in response | > 500 000 entries | Table growing unbounded |
| ETS table memory_mb in response | > 256 MB per table | Memory pressure from large cache |
| ETS table exists | false in response | Table not created; all Matcha queries fail |
| Sweeper heartbeat silence | Grace period exceeded | TTL eviction stopped; cache never clears |
Use Vigilmon's keyword check with a body match on "error" to catch individual table failures embedded in the JSON response.
Step 6: Status page
- Go to Status Pages → New in Vigilmon.
- Add monitors: "Application health" (HTTP) and "Session sweeper" (heartbeat).
- Share the URL in your team's incident-response runbook.
When a Matcha-backed ETS table starts filling up, the status page gives on-call engineers instant signal: the health check degrades before the OOM kill, and the heartbeat monitor shows whether the sweeper is still running.
Conclusion
Matcha's compile-time match specifications eliminate an entire class of runtime MatchSpec errors — but they cannot prevent your ETS tables from running out of memory or your maintenance workers from crashing. With Vigilmon you add the runtime safety layer:
- HTTP uptime checks that detect application and database failures
- ETS health data surfaced in your health endpoint via
EtsMonitor - Heartbeat monitors that confirm TTL sweepers keep running
- Keyword alerts that fire when any individual table exceeds capacity thresholds
Start with the health endpoint and one sweeper heartbeat, then add monitors for each critical ETS table as your Matcha usage grows.
Get started free at vigilmon.online.