How to Monitor Peep with Vigilmon
Peep is a high-performance Telemetry metric reporter for Elixir that supports StatsD, Prometheus, and custom reporter backends. Where PromEx couples metrics to Phoenix Dashboard and LiveDashboard, Peep is intentionally minimal: attach it to your :telemetry events and it routes measurements to whatever sink your infrastructure uses, with no framework dependency.
Peep handles metric export. It does not tell you whether your app is up, whether the reporter process is running, or whether the StatsD/Prometheus endpoint is reachable from outside. Vigilmon fills that gap with external uptime checks and heartbeat monitors.
This tutorial covers:
- A health check endpoint with Peep reporter status
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring to detect a stalled Peep reporter
- Alerts when metric export falls behind
Why Monitor Peep?
| Failure mode | Symptom without monitoring | |---|---| | Peep GenServer crashes | Metrics silently stop; dashboards go stale | | StatsD UDP socket unreachable | No error raised; data disappears | | Prometheus scrape endpoint returns 500 | Prometheus gaps; no alert to app team | | Telemetry event detach after hot code reload | Reporter no longer listening; silent data loss | | App unreachable | Metric reporter running but no real traffic |
Step 1: Add Peep and expose a health endpoint
Install Peep:
# mix.exs
{:peep, "~> 3.0"},
{:telemetry_metrics, "~> 1.0"},
Define your metrics and start a Peep reporter:
# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
import Telemetry.Metrics
def metrics do
[
# Phoenix request metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond},
tags: [:method, :status]
),
counter("phoenix.router_dispatch.stop.duration",
tags: [:route]
),
# Ecto query metrics
summary("my_app.repo.query.total_time",
unit: {:native, :millisecond},
tags: [:source]
),
# VM metrics
last_value("vm.memory.total", unit: :byte),
last_value("vm.total_run_queue_lengths.total"),
]
end
end
Start Peep in your supervision tree (Prometheus example):
# lib/my_app/application.ex
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{Peep, [
name: MyApp.Peep,
reporter: Peep.Reporters.Prometheus,
metrics: MyApp.Telemetry.metrics()
]},
]
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
Expose a Prometheus scrape endpoint via a plug:
# lib/my_app_web/plugs/metrics.ex
defmodule MyAppWeb.Plugs.Metrics do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/metrics"} = conn, _opts) do
case Peep.get_all_metrics(MyApp.Peep) do
{:ok, metrics} ->
conn
|> put_resp_content_type("text/plain; version=0.0.4")
|> send_resp(200, Peep.Reporters.Prometheus.scrape(metrics))
|> halt()
{:error, _} ->
conn
|> send_resp(503, "Peep reporter unavailable")
|> halt()
end
end
def call(conn, _opts), do: conn
end
Add a health check that includes Peep reporter status:
# 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 = %{
database: check_database(),
peep_reporter: check_peep(),
memory: check_memory()
}
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(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp check_peep do
case Process.whereis(MyApp.Peep) do
pid when is_pid(pid) ->
if Process.alive?(pid), do: :ok, else: :error
nil ->
:error
end
end
defp check_memory do
total_mb = :erlang.memory()[:total] / 1_048_576
if total_mb < 1_500, do: :ok, else: :error
end
end
Register both plugs before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Plugs.Metrics
plug MyAppWeb.Router
Test:
curl http://localhost:4000/health | jq .
# {"status":"ok","checks":{"database":"ok","peep_reporter":"ok","memory":"ok"}}
curl http://localhost:4000/metrics | head -10
# # HELP phoenix_endpoint_stop_duration_milliseconds ...
Step 2: 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. - Set alert threshold to 2 consecutive failures.
Add a second monitor for the Prometheus scrape endpoint if Prometheus is your primary metrics sink:
- URL:
https://your-app.example.com/metrics - Keyword check:
phoenix_endpoint_stop_duration(or any stable metric name)
This catches cases where the app is up but the Peep reporter has crashed.
Step 3: Heartbeat monitoring for the Peep reporter
The Peep GenServer can restart after a crash without anyone noticing the gap in exported metrics. A watchdog GenServer that pings Vigilmon only when Peep is running gives you an external signal.
# lib/my_app/peep_watchdog.ex
defmodule MyApp.PeepWatchdog do
use GenServer
require Logger
@interval :timer.seconds(60)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
@impl true
def init(state) do
schedule_check()
{:ok, state}
end
@impl true
def handle_info(:check, state) do
case check_peep_health() do
:ok ->
ping_heartbeat()
{:error, reason} ->
Logger.warning("Peep reporter unhealthy: #{inspect(reason)}")
end
schedule_check()
{:noreply, state}
end
defp check_peep_health do
case Process.whereis(MyApp.Peep) do
pid when is_pid(pid) ->
if Process.alive?(pid), do: :ok, else: {:error, :dead}
nil ->
{:error, :not_registered}
end
end
defp ping_heartbeat do
url = System.get_env("VIGILMON_PEEP_HEARTBEAT_URL")
if url do
Task.start(fn ->
:httpc.request(:get, {String.to_charlist(url), []}, [], [])
end)
end
end
defp schedule_check, do: Process.send_after(self(), :check, @interval)
end
Add to your supervision tree:
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{Peep, [name: MyApp.Peep, reporter: Peep.Reporters.Prometheus, metrics: MyApp.Telemetry.metrics()]},
MyApp.PeepWatchdog,
]
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat.
- Set grace period to 3 minutes (watchdog runs every 60 s with a 2x buffer).
- Copy the ping URL into
VIGILMON_PEEP_HEARTBEAT_URL.
Step 4: Key metrics to alert on
| Metric | Alert threshold | Why |
|---|---|---|
| /health HTTP status | Non-200 for 2 checks | App or reporter down |
| peep_reporter check | error in body | GenServer not alive |
| /metrics keyword check | Missing expected metric name | Peep scrape broken |
| Heartbeat silence | Grace period exceeded | Reporter crashed or app stopped |
Step 5: Status page
- In Vigilmon, go to Status Pages → New.
- Add your HTTP monitor,
/metricsmonitor, and Peep heartbeat. - Label them "App health", "Prometheus scrape", and "Peep reporter".
- Share the URL in your infrastructure runbook so the team can distinguish app downtime from metrics-pipeline failures.
Conclusion
Peep makes metric reporting fast and dependency-free. Vigilmon makes sure the reporter and the app it runs in are actually healthy. Together:
- Uptime checks detect when your app is unreachable
- Metrics endpoint monitor catches silent Peep reporter failures
- Heartbeat monitors alert when the reporter goes dark between scrapes
Start with the /health endpoint and the Peep watchdog heartbeat, then add a /metrics keyword monitor once Prometheus scraping is stable.
Get started free at vigilmon.online.