Monitoring Your Keila Instance with Vigilmon
Keila is a self-hosted email newsletter and mailing list management platform — a GDPR-focused alternative to Mailchimp, built on Elixir/Phoenix. It runs on port 4000 and handles everything from campaign creation to subscriber analytics.
Email marketing infrastructure has high stakes: a failed campaign send wastes subscriber goodwill, broken unsubscribe links expose you to compliance risk, and a stalled send queue can make scheduled campaigns miss their window entirely. These failures are often silent — the web UI looks fine while the sending pipeline is backed up or the email delivery backend is unreachable.
This tutorial sets up production monitoring for Keila using Vigilmon:
- Phoenix web server availability
- PostgreSQL database connectivity
- Campaign send pipeline health
- Email delivery backend (SMTP/SES/Sendgrid) connectivity
- Subscriber confirmation flow
- Campaign analytics tracking endpoint
- Unsubscribe processing health
- Scheduled campaign job heartbeats
- TLS certificate expiry
- Slack alerting
Step 1: Add a health check endpoint
Keila is built on Phoenix (Elixir). Add a lightweight health check plug that checks the database and returns a structured response.
# lib/keila_web/plugs/health_check.ex
defmodule KeilaWeb.Plugs.HealthCheck do
import Plug.Conn
require Logger
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = %{
database: check_database(),
oban: check_oban()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(%{
status: (if status == 200, do: "ok", else: "degraded"),
checks: checks
}))
|> halt()
end
def call(conn, _opts), do: conn
defp check_database do
case Ecto.Adapters.SQL.query(Keila.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
{:error, reason} ->
Logger.error("Health check: DB error: #{inspect(reason)}")
:error
end
end
defp check_oban do
# Verify Oban queue table is accessible (campaign send jobs land here)
case Ecto.Adapters.SQL.query(Keila.Repo, "SELECT count(*) FROM oban_jobs WHERE state = 'available'", []) do
{:ok, _} -> :ok
{:error, _} -> :error
end
end
end
Add it to your endpoint before the router:
# lib/keila_web/endpoint.ex
defmodule KeilaWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :keila
plug KeilaWeb.Plugs.HealthCheck # ← before the router
plug KeilaWeb.Router
end
Test it:
curl http://localhost:4000/health
# {"status":"ok","checks":{"database":"ok","oban":"ok"}}
Step 2: Monitor Phoenix web server availability
Point Vigilmon at your health endpoint:
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- URL:
https://newsletter.yourdomain.com/health - Check interval: 1 minute
- Expected status: 200
- Save
Keila's Phoenix server is highly reliable, but crashes do happen — especially after OOM events or bad deploys. This catches them within 60 seconds.
Step 3: Monitor the campaign send pipeline
Keila uses Oban (an Elixir job queue backed by PostgreSQL) to process campaign sends. The health check in Step 1 already verifies Oban's table is accessible. Add a second layer: monitor whether the Oban queue is processing or stalled.
A stalled queue typically appears as a growing number of executing or available jobs with no progress. Add a metric endpoint to your health check that exposes the queue depth:
# In your health check, add queue depth
defp check_oban do
case Ecto.Adapters.SQL.query(
Keila.Repo,
"SELECT state, count(*) FROM oban_jobs WHERE queue = 'mailer' GROUP BY state",
[]
) do
{:ok, %{rows: rows}} ->
executing = Enum.find_value(rows, 0, fn [state, count] -> if state == "executing", do: count end)
# Alert if more than 100 mailer jobs stuck in executing for a while
if executing > 100, do: :degraded, else: :ok
{:error, _} ->
:error
end
end
You can also create a dedicated Vigilmon HTTP monitor for a queue status endpoint and set a response body keyword check to catch queue stall conditions.
Step 4: Monitor email delivery backend connectivity
Keila sends emails through a configured SMTP relay, SendGrid, Amazon SES, or similar. If the delivery backend goes down, Keila's queue fills up but no emails leave. Monitor the backend independently from the application.
For SMTP (e.g. Postfix):
In Vigilmon:
- Click New Monitor → TCP
- Host: your SMTP server hostname
- Port: 25 (SMTP) or 587 (submission) or 465 (SMTPS)
- Save
For SendGrid / Amazon SES / API-based backends:
Add an HTTP monitor to the provider's status API:
- SendGrid:
https://status.sendgrid.com/api/v2/status.json— keyword check for"indicator":"none" - Amazon SES:
https://health.aws.amazon.com/health/status— keyword check confirming your region is healthy
Probe your actual sending ability with a heartbeat pattern:
# lib/keila/monitoring/delivery_probe.ex
defmodule Keila.Monitoring.DeliveryProbe do
require Logger
@heartbeat_url System.get_env("VIGILMON_DELIVERY_HEARTBEAT_URL")
def run do
case send_probe_email() do
:ok ->
if @heartbeat_url, do: HTTPoison.get(@heartbeat_url, [], recv_timeout: 5_000)
{:error, reason} ->
Logger.error("Delivery probe failed: #{inspect(reason)}")
end
end
defp send_probe_email do
# Use Keila's mailer to send a probe email to a sink address
# (configure a no-reply sink that accepts everything)
Keila.Mailer.deliver_now(%Swoosh.Email{
from: {"Probe", "probe@yourdomain.com"},
to: [{"Sink", "probe-sink@yourdomain.com"}],
subject: "Delivery Probe #{DateTime.utc_now()}",
text_body: "probe"
})
end
end
Schedule this via Oban every hour and pair it with a Vigilmon heartbeat monitor (see Step 8).
Step 5: Monitor the subscriber confirmation flow
Double opt-in confirmation emails must be delivered reliably — they're the gate between a new subscriber and your list. If these fail, you're accumulating unconfirmed subscribers and potentially losing real ones.
Monitor the confirmation endpoint itself:
- URL:
https://newsletter.yourdomain.com/subscribe/confirm/probe-token-that-does-not-exist - Expected status: 404 or 200 — either confirms the route exists and the server is handling confirmation requests
For a functional probe, watch your email delivery logs for confirmation emails and alert if the delivery rate drops:
# In your log monitoring system
# Alert if confirmation email job queue grows without processing
Step 6: Monitor the campaign analytics tracking endpoint
Email open and click tracking are how Keila measures campaign performance. If the tracking endpoint goes down mid-campaign, you lose analytics data for the entire send.
Monitor the tracking pixel endpoint:
- URL:
https://newsletter.yourdomain.com/track/open/probe-campaign-id/probe-subscriber-id - Expected status: 200 or 404
A 404 here is fine — it means the router handled the request and couldn't find the probe IDs, which is expected behavior. What matters is that the tracking route is alive.
Step 7: Monitor unsubscribe processing
Unsubscribe requests must be processed immediately — GDPR and CAN-SPAM compliance depend on it. Monitor the unsubscribe endpoint:
- URL:
https://newsletter.yourdomain.com/unsubscribe/probe-token-that-does-not-exist - Expected status: 200 or 404
Any 500, 502, or timeout here is a compliance risk, not just a usability issue. Set the alert threshold to 0 consecutive failures — a single failure should alert.
Step 8: Heartbeat monitoring for scheduled campaign sends
Scheduled campaigns (campaigns set for future delivery) rely on an Oban cron job to trigger the send at the right time. If this job stops running, campaigns will be silently skipped.
Add a heartbeat to your Oban periodic job:
# In your Oban worker for scheduled campaign dispatch
defmodule Keila.Workers.ScheduledCampaignWorker do
use Oban.Worker, queue: :scheduler
require Logger
@heartbeat_url System.get_env("VIGILMON_SCHEDULER_HEARTBEAT_URL")
@impl Oban.Worker
def perform(%Oban.Job{}) do
with :ok <- dispatch_due_campaigns() do
if @heartbeat_url do
case HTTPoison.get(@heartbeat_url, [], recv_timeout: 5_000) do
{:ok, _} -> :ok
{:error, reason} -> Logger.warning("Heartbeat ping failed: #{inspect(reason)}")
end
end
:ok
end
end
defp dispatch_due_campaigns do
# Your existing logic to find and dispatch due campaigns
:ok
end
end
In Vigilmon:
- Click New Monitor → Heartbeat
- Name:
Keila Scheduled Campaign Dispatcher - Expected interval: 5 minutes (if your Oban cron runs every 5 minutes)
- Grace period: 5 minutes
- Copy the ping URL → set as
VIGILMON_SCHEDULER_HEARTBEAT_URL - Save
Also add a heartbeat for the delivery probe from Step 4:
- Click New Monitor → Heartbeat
- Name:
Keila Email Delivery Probe - Expected interval: 1 hour
- Grace period: 10 minutes
Step 9: TLS certificate expiry monitoring
Newsletter links and unsubscribe URLs embed your domain. If your TLS certificate expires, every link in previously sent emails becomes broken, and subscriber confirmation emails can't be delivered over HTTPS.
In Vigilmon:
- Go to New Monitor → SSL Certificate
- Domain:
newsletter.yourdomain.com - Alert thresholds: 30 days (warning) and 7 days (critical)
- Save
Step 10: Slack alerts
In Vigilmon, go to Notifications → New Channel → Slack and paste your Slack incoming webhook URL.
Enable Slack on all Keila monitors. The most critical alert patterns:
🔴 DOWN: newsletter.yourdomain.com/health
Status: 503 Service Unavailable
Detected: 2 minutes ago
🔴 MISSED HEARTBEAT: Keila Scheduled Campaign Dispatcher
Last ping: 47m ago (expected every 5m)
🔴 MISSED HEARTBEAT: Keila Email Delivery Probe
Last ping: 3h 20m ago (expected every 1h)
A missed heartbeat on the scheduler while you have campaigns scheduled is a high-priority incident — your audience is waiting for an email that won't arrive.
What you've built
| What | How |
|------|-----|
| Phoenix web server | Vigilmon HTTP monitor → /health |
| PostgreSQL database | Health endpoint DB check + Oban table check |
| Campaign send pipeline | Oban queue depth in health check |
| SMTP delivery backend | TCP monitor on port 587 |
| API delivery backend | HTTP monitor → provider status API |
| Actual delivery ability | Probe email + hourly heartbeat |
| Subscriber confirmation | HTTP monitor → /subscribe/confirm/ expecting 404 |
| Analytics tracking | HTTP monitor → /track/open/ expecting 404 |
| Unsubscribe processing | HTTP monitor → /unsubscribe/ expecting 404 |
| Scheduled campaign dispatch | Heartbeat, 5-minute expected interval |
| TLS certificate | SSL monitor, 30-day + 7-day alert |
| Slack alerts | Vigilmon Slack notification channel |
Keila handles the trust your subscribers placed in you. Vigilmon handles the trust your team places in Keila.
Next steps
- Add response time monitoring on the campaign dashboard to catch slow PostgreSQL queries before they affect your team's workflow
- Set up separate Oban queue monitors for each queue (mailer, scheduler, analytics) — a stall in one queue shouldn't mask stalls in others
- Add a Vigilmon status page so your team has a single source of truth during incidents
- If you're sending high-volume campaigns, add Vigilmon's multi-region checks to catch DNS propagation issues that affect deliverability
Get started free at vigilmon.online.