How to Monitor Canada with Vigilmon
Canada is an authorization library for Elixir that implements the CanCan/CanCanCan policy pattern via the Canada.Can protocol. It enables resource-level permission checks — Canada.Can.can?(user, :edit, post) — with a clean separation between authorization policy and business logic. It's commonly used in Phoenix applications alongside Ecto to enforce who can do what with which resources.
The monitoring challenge: authorization is invisible when it works and catastrophic when it doesn't. A misconfigured policy that blocks all admins looks identical to a login bug from the outside. A policy that grants too much access doesn't show up in your error logs at all.
This tutorial sets up monitoring for a Canada-powered Phoenix application:
- A health endpoint that validates core authorization policies
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for authorization-gated background workers
- Alerts on authorization failures and policy regressions
Why Monitor Canada?
| Signal | What it catches |
|---|---|
| Policy module availability | Deploy failures that remove or corrupt policy modules |
| Authorization denial rate | Policy regressions that over-restrict or under-restrict access |
| Admin access health | Misconfigured admin policies locking out operators |
| Background job authorization | Workers failing because their service account lost permissions |
| Protocol implementation | Missing Canada.Can implementations for new resource types |
Authorization bugs are security bugs. Monitoring your authorization layer is not optional in production.
Step 1: Implement Canada.Can for Your Resources
Define your authorization policies:
# lib/my_app/policies/post_policy.ex
defimpl Canada.Can, for: MyApp.Accounts.User do
def can?(%{role: :admin}, _action, _resource), do: true
def can?(%{id: user_id}, action, %MyApp.Blog.Post{author_id: author_id})
when action in [:edit, :delete] do
user_id == author_id
end
def can?(_user, :read, %MyApp.Blog.Post{published: true}), do: true
def can?(_user, :create, MyApp.Blog.Post), do: true
def can?(_user, _action, _resource), do: false
end
Use it in your Phoenix controllers:
# lib/my_app_web/controllers/post_controller.ex
defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
def edit(conn, %{"id" => id}) do
post = MyApp.Blog.get_post!(id)
user = conn.assigns.current_user
if Canada.Can.can?(user, :edit, post) do
render(conn, :edit, post: post)
else
conn
|> put_status(:forbidden)
|> put_view(MyAppWeb.ErrorView)
|> render(:"403")
|> halt()
end
end
end
Step 2: Add a Policy Health Check Endpoint
Create an endpoint that validates your core authorization policies against fixture data. This catches policy module regressions on deploy:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
case check_authorization_policies() do
:ok ->
json(conn, %{status: "ok", authorization: "policies_valid"})
{:error, failures} ->
conn
|> put_status(503)
|> json(%{status: "error", authorization: "policy_regression", failures: failures})
end
end
defp check_authorization_policies do
admin = %MyApp.Accounts.User{id: 1, role: :admin}
user = %MyApp.Accounts.User{id: 2, role: :user}
own_post = %MyApp.Blog.Post{id: 1, author_id: 2, published: true}
other_post = %MyApp.Blog.Post{id: 2, author_id: 99, published: true}
draft_post = %MyApp.Blog.Post{id: 3, author_id: 99, published: false}
checks = [
{"admin can edit any post", Canada.Can.can?(admin, :edit, own_post), true},
{"user can edit own post", Canada.Can.can?(user, :edit, own_post), true},
{"user cannot edit other post", Canada.Can.can?(user, :edit, other_post), false},
{"user can read published post", Canada.Can.can?(user, :read, own_post), true},
{"user cannot read draft by other", Canada.Can.can?(user, :read, draft_post), false}
]
failures =
checks
|> Enum.reject(fn {_label, result, expected} -> result == expected end)
|> Enum.map(fn {label, result, expected} ->
"#{label}: got #{result}, expected #{expected}"
end)
if failures == [], do: :ok, else: {:error, failures}
end
end
Add the route:
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
pipe_through :api
get "/health", HealthController, :index
end
Test it:
curl http://localhost:4000/health
# => {"status":"ok","authorization":"policies_valid"}
Step 3: Add Vigilmon HTTP Monitoring
- Log in to vigilmon.online and click Add Monitor.
- Set Type to
HTTP / HTTPS. - Enter your health endpoint:
https://yourapp.com/health. - Set Check interval to
1 minute. - Set Expected HTTP status to
200. - Under Advanced, add Response body contains:
"authorization":"policies_valid". - Click Save.
When a deploy breaks a policy — say, a refactor that reorganizes user struct fields — the health check catches it within a minute.
Step 4: Track Authorization Denials
Add a Plug that counts 403 responses to catch denial rate spikes:
# lib/my_app_web/plugs/authorization_metrics.ex
defmodule MyAppWeb.Plugs.AuthorizationMetrics do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
register_before_send(conn, fn conn ->
if conn.status == 403 do
:telemetry.execute([:my_app, :authorization, :denied], %{count: 1}, %{
path: conn.request_path,
method: conn.method
})
end
conn
end)
end
end
Attach a telemetry handler to track denials in your application:
# lib/my_app/application.ex
:telemetry.attach(
"authorization-denied",
[:my_app, :authorization, :denied],
fn _event, _measurements, metadata, _config ->
MyApp.AuthMetrics.record_denial(metadata.path)
end,
nil
)
Register the plug in your endpoint:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.AuthorizationMetrics
plug MyAppWeb.Router
Step 5: Heartbeat Monitoring for Authorization-Gated Background Jobs
Background workers that require authorization (e.g., a job that acts as a specific service user) can silently fail if their permissions are revoked:
# lib/my_app/background_job_heartbeat.ex
defmodule MyApp.BackgroundJobHeartbeat do
use GenServer
@interval :timer.minutes(5)
@vigilmon_heartbeat_url System.get_env("VIGILMON_JOB_HEARTBEAT_URL") ||
"https://vigilmon.online/api/heartbeat/YOUR_HEARTBEAT_ID"
def start_link(_), do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def init(_) do
schedule()
{:ok, nil}
end
def handle_info(:ping, state) do
service_user = MyApp.Accounts.get_service_user!()
authorized = Canada.Can.can?(service_user, :process, MyApp.Jobs.DataExport)
if authorized do
:httpc.request(:get, {String.to_charlist(@vigilmon_heartbeat_url), []}, [], [])
end
schedule()
{:noreply, state}
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
To create the heartbeat in Vigilmon:
- Click Add Monitor → Cron / Heartbeat.
- Set expected interval to
5 minutes. - Copy the heartbeat URL to
VIGILMON_JOB_HEARTBEAT_URL.
If the service user's role is accidentally changed, the heartbeat stops and Vigilmon alerts you.
Step 6: Set Up Alerts
Configure alert channels:
- Go to Alert Channels → Add Channel.
- Choose Slack, Email, or PagerDuty.
- Set alert threshold to 1 consecutive failure for authorization policy checks (policy bugs are high-severity).
- For the heartbeat, set Missing heartbeat to alert after 2 missed pings (10 minutes).
- Assign channels to all monitors.
For admin-accessible applications, also set up a Status Page so operators can verify access control health independently:
- Go to Status Pages → Create Page.
- Add your health monitor.
- Share with your ops team — they can check if the app is up before assuming it's an access issue.
Key Metrics to Watch
| Metric | Vigilmon feature | What to alert on |
|---|---|---|
| Policy availability | HTTP monitor on /health | Any 5xx or body mismatch |
| Application availability | HTTP monitor on /health | Any non-200 or timeout |
| Authorization denial rate | Tracked via Plug metrics + heartbeat | Spike above baseline |
| Background job authorization | Heartbeat monitor | Any missed heartbeat |
| Response time | Response time chart | P95 > 500ms |
| SSL certificate | Cert expiry monitor | Expires in < 14 days |
Conclusion
Canada's protocol-based authorization is elegant and composable — but authorization regressions are security incidents, not just bugs. By running policy fixture checks on every deploy and monitoring them with Vigilmon, you get immediate notification when access control breaks. The heartbeat pattern extends this to background workers, catching the silent case where a service account quietly loses its permissions.
Get started free at vigilmon.online.