How to Monitor Ecto with Vigilmon
Ecto is the de facto data layer for Elixir applications. It provides changesets, schemas, a composable query DSL, and connection pooling through DBConnection — all the plumbing you need to talk to PostgreSQL, MySQL, or SQLite. But when the database goes slow or the pool runs dry, Ecto doesn't shout. Queries time out, changesets fail silently, and users see errors that don't surface in your application logs until it's too late.
External monitoring catches what Ecto can't self-report. This tutorial covers:
- A health endpoint that validates Ecto connectivity and pool state
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring to detect when background jobs using Ecto stall
- Slack alerts and a status page
Step 1: Add Ecto to your project
If Ecto isn't already in your project:
# mix.exs
defp deps do
[
{:ecto_sql, "~> 3.11"},
{:postgrex, ">= 0.0.0"}, # or {:myxql, ">= 0.0.0"} for MySQL
{:req, "~> 0.4"} # for heartbeat pings
]
end
Configure your repo:
# config/config.exs
config :my_app, MyApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "my_app_dev",
pool_size: 10,
queue_target: 50,
queue_interval: 1000
The queue_target and queue_interval settings control how long DBConnection waits before raising a checkout error — tuning these gives you early warning of pool pressure before users notice.
Step 2: Build a health endpoint that includes Ecto state
Add a plug that checks database connectivity and pool availability:
# 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
checks = run_checks()
status = if all_ok?(checks), do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(checks))
|> halt()
end
def call(conn, _opts), do: conn
defp run_checks do
db = database_check()
%{
status: status_label(db.status == "ok"),
database: db
}
end
defp database_check do
start = System.monotonic_time(:millisecond)
result =
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", [], timeout: 3_000) do
{:ok, _} ->
elapsed = System.monotonic_time(:millisecond) - start
pool_info = pool_stats()
%{
status: "ok",
latency_ms: elapsed,
pool: pool_info
}
{:error, %DBConnection.ConnectionError{message: msg}} ->
%{status: "error", reason: msg}
{:error, reason} ->
%{status: "error", reason: inspect(reason)}
end
result
end
defp pool_stats do
# DBConnection.ConnectionPool exposes telemetry but not a direct stats API.
# Read pool_size from config as a reference point.
pool_size = Application.get_env(:my_app, MyApp.Repo)[:pool_size] || 10
%{configured_size: pool_size}
end
defp all_ok?(checks), do: checks.status == "ok"
defp status_label(true), do: "ok"
defp status_label(false), do: "degraded"
end
Register the plug in your endpoint before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
mix phx.server
curl http://localhost:4000/health | jq .
# {
# "status": "ok",
# "database": {
# "status": "ok",
# "latency_ms": 3,
# "pool": {"configured_size": 10}
# }
# }
A 503 with "status": "degraded" tells you the database is unreachable or too slow — before Vigilmon even alerts you.
Step 3: Capture slow queries with Ecto telemetry
Ecto emits telemetry events for every query. Attach a handler to log slow queries and expose a count in your health response:
# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
require Logger
@slow_query_threshold_ms 200
def setup do
:telemetry.attach(
"ecto-slow-query-logger",
[:my_app, :repo, :query],
&handle_query/4,
nil
)
end
def handle_query(_event, %{total_time: total_time}, %{query: query, source: source}, _config) do
duration_ms = System.convert_time_unit(total_time, :native, :millisecond)
if duration_ms >= @slow_query_threshold_ms do
Logger.warning(
"[Ecto slow query] #{duration_ms}ms — source=#{source} query=#{String.slice(query, 0, 200)}"
)
end
end
end
Call MyApp.Telemetry.setup() in your application.ex:
# lib/my_app/application.ex
def start(_type, _args) do
MyApp.Telemetry.setup()
# ... rest of supervision tree
end
Slow query logs flow to your log aggregator, and you can set up a Vigilmon keyword monitor on a log-shipping health endpoint or use Vigilmon's HTTP monitor with a latency threshold to catch degraded response times.
Step 4: Monitor the health endpoint with Vigilmon
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute
- Enable keyword check: body must contain
"status":"ok" - Save
The keyword check matters here: a misconfigured read replica might return HTTP 200 while Ecto is writing to the wrong host. The keyword match on "status":"ok" ensures the actual database ping succeeded, not just the web process.
Step 5: Heartbeat monitoring for background Ecto tasks
If you run periodic jobs that use Ecto — data cleanups, report generation, import pipelines — a heartbeat tells you the job ran and its database queries succeeded:
# lib/my_app/workers/db_heartbeat_worker.ex
defmodule MyApp.Workers.DbHeartbeatWorker do
use GenServer
require Logger
@interval :timer.minutes(5)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:run, state) do
run_check()
schedule()
{:noreply, state}
end
defp run_check do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} ->
ping_vigilmon()
{:error, reason} ->
Logger.error("Ecto heartbeat DB check failed: #{inspect(reason)}")
end
end
defp ping_vigilmon do
url = Application.get_env(:my_app, :vigilmon)[:heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, %{status: 200}} -> :ok
error -> Logger.warning("Vigilmon heartbeat ping failed: #{inspect(error)}")
end
end
end
defp schedule, do: Process.send_after(self(), :run, @interval)
end
Add it to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.Workers.DbHeartbeatWorker
]
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval: 10 minutes
- Copy the ping URL
- Set it as
VIGILMON_HEARTBEAT_URLin your environment
If the GenServer crashes, the Repo is unavailable, or migrations block pool checkout, the heartbeat stops — and Vigilmon alerts you within one missed interval.
Step 6: Expose migration state in the health check
Pending migrations are a common cause of startup failures and confusing query errors. Add a migration check:
# Add to your health check plug
defp migration_check do
case Ecto.Migrator.migrations(MyApp.Repo) do
migrations when is_list(migrations) ->
pending = Enum.count(migrations, fn {status, _, _} -> status == :down end)
status = if pending == 0, do: "ok", else: "warning"
%{status: status, pending_migrations: pending}
_ ->
%{status: "unknown"}
end
end
Add it to run_checks/0:
defp run_checks do
db = database_check()
migrations = migration_check()
overall_ok = db.status == "ok" and migrations.status in ["ok", "unknown"]
%{
status: status_label(overall_ok),
database: db,
migrations: migrations
}
end
Set a Vigilmon keyword alert to fire when the body contains "status":"warning" so you're notified of pending migrations before they cause runtime errors.
Step 7: Slack alerts and status page
Slack alerts:
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- Enable the channel on your Ecto health monitor and heartbeat monitor
When the database goes down:
🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
2 minutes ago
When the heartbeat times out:
🔴 HEARTBEAT MISSED: Ecto DB Heartbeat
Expected ping within 10 minutes — none received
Last seen: 14 minutes ago
Status page:
- Go to Status Pages → New Status Page
- Add your HTTP monitor and heartbeat monitor
- Share the URL with your team
What you've built
| What | How |
|------|-----|
| Health endpoint | Plug with Ecto ping + latency measurement |
| Keyword check | Vigilmon keyword match on "status":"ok" |
| Uptime monitoring | Vigilmon HTTP monitor → /health |
| DB liveness | Heartbeat GenServer + Vigilmon heartbeat monitor |
| Slow query detection | Ecto telemetry handler with threshold logging |
| Migration alerting | Pending migration count in health response |
| Slack alerts | Vigilmon Slack notification channel |
| Status page | Vigilmon public status page |
Ecto keeps your data layer solid. Vigilmon tells you when the foundation cracks.
Next steps
- Add per-Repo monitors if your app uses multiple Ecto repos (primary + read replica)
- Use Vigilmon's response time history to detect gradual query degradation before it becomes an outage
- Alert on
latency_msthresholds by combining the health endpoint with Vigilmon's body-content checks
Get started free at vigilmon.online.