Exposing a /metrics endpoint in Prometheus format, Grafana dashboard setup for heartbeat run counts, agent costs per hour, and error rate. PagerDuty/webhook alerting on anomalies.
Set up health checks, configure uptime monitoring, export metrics to Prometheus/Grafana, and define the three most important alerts.
# Liveness: is the process running?
GET /health
→ {"status": "ok"}
# Readiness: is the database connected?
GET /health/ready
→ {"status": "ok", "db": "connected", "latency_ms": 2}
# Heartbeat stats: last N runs
GET /health/heartbeat
→ {
"last_run_at": "...",
"last_run_status": "completed",
"runs_last_hour": 12,
"issues_resolved_last_hour": 47,
"errors_last_hour": 0
}
Point an uptime monitor (Better Uptime, UptimeRobot, Freshping) at /health/ready:
URL: https://your-domain.com/health/ready
Interval: 1 minute
Expected status: 200
Alert: email + Slack when 2 consecutive checks fail
Enable Prometheus export in .env:
PROMETHEUS_ENABLED=true
PROMETHEUS_PORT=9090
Available metrics:
paperclip_heartbeat_runs_total{status="completed"} 142
paperclip_heartbeat_runs_total{status="failed"} 3
paperclip_issues_resolved_total{agent="Researcher"} 89
paperclip_cost_usd_total{agent="Researcher", model="claude-opus-4-5"} 1.23
paperclip_agent_status{agent="Researcher"} 1 # 1=active, 0=paused
Grafana dashboard JSON is included in packages/server/monitoring/grafana-dashboard.json.
services:
api:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3100/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
-- Check every 5 minutes
SELECT name FROM companies WHERE paused = true;
Alert if any company is paused unexpectedly.
-- Check every 15 minutes
SELECT COUNT(*) FROM heartbeat_runs
WHERE created_at > now() - interval '15 minutes'
AND status = 'failed';
Alert if >2 failures in the last 15 minutes.
-- Check every hour
SELECT COUNT(*) FROM issues
WHERE status = 'open'
AND created_at < now() - interval '1 hour';
Alert if >50 open issues are more than 1 hour old (heartbeat may be stuck).
In production, ship API logs to a log aggregator:
# docker-compose.yml
services:
api:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Or send to Loki:
# driver: loki
# options:
# loki-url: "http://loki:3100/loki/api/v1/push"