tutorial

Monitoring Your Keila Instance with Vigilmon

Keep your self-hosted email newsletter platform healthy with uptime checks, campaign pipeline monitoring, email delivery backend health, subscriber flow monitoring, and heartbeat checks for scheduled sends.

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:

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. URL: https://newsletter.yourdomain.com/health
  4. Check interval: 1 minute
  5. Expected status: 200
  6. 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:

  1. Click New Monitor → TCP
  2. Host: your SMTP server hostname
  3. Port: 25 (SMTP) or 587 (submission) or 465 (SMTPS)
  4. 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:

  1. Click New Monitor → Heartbeat
  2. Name: Keila Scheduled Campaign Dispatcher
  3. Expected interval: 5 minutes (if your Oban cron runs every 5 minutes)
  4. Grace period: 5 minutes
  5. Copy the ping URL → set as VIGILMON_SCHEDULER_HEARTBEAT_URL
  6. Save

Also add a heartbeat for the delivery probe from Step 4:

  1. Click New Monitor → Heartbeat
  2. Name: Keila Email Delivery Probe
  3. Expected interval: 1 hour
  4. 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:

  1. Go to New Monitor → SSL Certificate
  2. Domain: newsletter.yourdomain.com
  3. Alert thresholds: 30 days (warning) and 7 days (critical)
  4. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →