Monitoring

SkillMonitoring & ops

Use when setting up uptime and health monitoring, alerts, or on-call basics for a service already in production, so you learn it is down before customers do — health and readiness probes, alerting on SLO error-budget burn rather than raw counts, curing alert fatigue, on-call rotation with escalation, and a status page. NOT instrumenting logs, metrics or traces, or wiring OpenTelemetry (that is `observability`).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Monitoring skill

What this skill tells your AI

The instructions your AI receives, as published by ericrisco/rsc-harness in skills/monitoring/SKILL.md and read by ahel’s review.

You are wiring up the outside view of a service that already shipped: is it alive, is it fast, and when it breaks, does exactly one human get exactly one actionable page. This skill emits a concrete setup — a checker config, a health-endpoint contract, symptom-based alert rules, and an on-call rotation. Not telemetry instrumentation (that is ../observability/SKILL.md), not the release-gating healthcheck (that is ../deployment/SKILL.md).

The one rule

A page is justified only when there is real user impact AND a human action the system can't take itself. Everything below descends from this. Internalize the three tiers:

  • Page (wake someone): users are hurting now and a human must intervene. Checkout returns 5xx. Site unreachable. Error budget burning fast.
  • Ticket (look during business hours): degraded but not bleeding. Slow-burn budget use, cert expiring in 14 days.
  • Dashboard-only (don't notify): CPU at 80%, a single retry, a transient blip the system already healed.

If an alert doesn't map to an immediate human action, it is not a page — it's noise, and noise trains people to ignore the one page that matters.

The 4-layer stack

Don't skip layers and don't collapse them — each answers a different question.

LayerAnswersBuilt withFires when
1. External uptime probe"Is it reachable from the outside?"Uptime Kuma / UptimeRobot / Better StackURL down, TLS broken, p95 latency over budget
2. Health endpoints"Is the process alive, and are its deps reachable?"/livez + /readyz on the serviceliveness fails → restart; readiness fails → pull from rotation
3. SLO burn-rate alert"Are we spending the error budget too fast?"Prometheus/Grafana/Better Stack rulemulti-window burn rate exceeds threshold
4. On-call escalation"Who acts, and who's the backup?"PagerDuty / incident.io / Better Stacka page from layers 1–3 routes + escalates

Layer 1 catches "the whole thing is gone." Layer 2 catches "a dependency died" before users do. Layer 3 catches "we're degrading faster than we can afford." Layer 4 makes sure a human shows up.

Pick a tool

Decide by budget, team size, and self-host appetite. Pricing as of 2026-06.

ToolFree tierCheck intervalBest forPaid from
Uptime Kuma 2.1.3Fully free, self-hosteddown to ~20sfull control, you have a VPS$0 (you pay the box)
UptimeRobot50 monitors @ 5-min, 1 status page5-min free / 1-min paidno infra, want managed$7/mo
Better Stack10 monitors + incidents + logs30sincident mgmt + on-call in one$24/mo
Pingdomnonesub-minuteenterprise synthetic + RUM$15/mo

Default: Uptime Kuma if you already run a VPS (1 GB box is comfortable; needs ~400 MB RAM), UptimeRobot free tier if you don't want infra. Kuma 2.1 added Globalping worldwide probe locations (so you test from regions, not just your one box) and built-in domain-expiry monitors. Concrete docker-compose and notification wiring live in references/tool-setup.md.

Health endpoints done right

Split liveness from readiness — they trigger different machine actions.

  • /livez (liveness): is the process healthy? If it fails, the orchestrator restarts the container. Keep it dumb: just "am I running and not deadlocked." Never check the database here.
  • /readyz (readiness): are dependencies reachable so I can serve traffic? If it fails, the orchestrator pulls this instance from the load-balancer rotation but does not kill it.

The cheap-probe rule: a probe runs constantly, so it must be <100ms and must not cascade-check every downstream. Why: if /livez pings the DB and the DB is briefly slow, liveness fails, the container restarts, the restart hammers the recovering DB — a restart loop that turns a 30-second blip into an outage.

# Bad — one /health that cascades and returns 500 on any hiccup.
# A slow Redis takes the whole service down and triggers restart loops.
@app.get("/health")
def health():
    db.execute("SELECT 1")          # blocks
    redis.ping()                    # blocks
    requests.get(PAYMENTS_URL)      # blocks on a third party!
    return {"status": "ok"}         # 500 if ANY of these throws
# Good — split, cheap, correct status codes, small JSON.
@app.get("/livez")                  # liveness: process only. Restart if this fails.
def livez():
    return {"status": "alive"}      # 200, ~1ms, touches nothing downstream

@app.get("/readyz")                 # readiness: deps with short timeouts. Pull from LB if this fails.
def readyz():
    checks = {"db": ping(db, timeout=0.2), "cache": ping(redis, timeout=0.1)}
    ok = all(checks.values())
    return JSONResponse(
        {"status": "ready" if ok else "degraded", "checks": checks},
        status_code=200 if ok else 503,   # 503 so the LB pulls this instance
    )

Do not put a third-party API call in readiness — a payment provider's outage shouldn't pull all your instances from rotation and take you fully down. Degrade that path in code (../error-handling/SKILL.md), don't fail-closed on it here. Go and FastAPI handler examples are in references/tool-setup.md.

What to actually monitor

The golden checklist. Monitor the symptom users feel, not just that one URL returns 200.

  • Availability — the critical endpoint(s) reachable, from multiple regions.
  • Latency p95/p99 — averages hide the tail; alert on p95/p99 against a budget, not the mean.
  • Error rate — the 5xx-to-total ratio, because raw 5xx count says nothing without traffic volume.
  • SSL cert expiry — alert at 14 days; an expired cert is a full outage that no app metric catches.
  • Domain expiry — alert at 30 days; a lapsed domain is the most embarrassing avoidable outage.
  • The critical user journey — a synthetic check that does login → core action → result, NOT just GET /. The homepage can be 200 while checkout is broken.

The homepage being up tells you almost nothing. Probe the path that makes you money.

Alerts that don't cry wolf

Alert on symptoms, not causes. Page on "checkout error rate >2% for 5 min" (user impact), not on "CPU >80%" (a cause that may be harmless and self-resolving). High CPU with happy users is a dashboard line, not a 3am page.

Use multi-window, multi-burn-rate for SLO alerts (Google SRE workbook). Burn rate = how fast you're spending the monthly error budget. Require a long and a short window to both fire — the long window says "this is real," the short window says "this is still happening," and together they kill false positives from a single spike.

Burn rateWindowBudget spentSeverityAction
> 14.41h (+ 5m short)2% in 1hcriticalpage
> 66h (+ 30m short)5% in 6hwarningticket
> 13d (+ 6h short)10% in 3dinforeview

Routing: critical → page, warning → ticket, info → dashboard/review. Dedupe and group related alerts into one incident (10 hosts failing the same check = one page, not ten). Set maintenance windows so planned deploys don't page anyone. The full burn-rate math and a copy-paste Prometheus-style rule with a runbook_url annotation are in references/burn-rate-and-oncall.md.

On-call basics

  • Rotation: weekly, with a primary and a secondary. One person can't be the single point of failure for the system that catches single points of failure.
  • Escalation policy: page primary → if no ack in 5–10 min, page secondary → then the manager. No-ack must always hop; an unacked page is a dropped page.
  • Runbook per alert: every alert links to a runbook with five fields — Symptom, Impact, First 3 checks, Mitigation, How to escalate. The person paged at 3am should not have to think from scratch. Template in references/burn-rate-and-oncall.md.
  • Status page + comms: a public status page (Kuma and Better Stack include one) so customers self-serve "is it you or me," cutting inbound during an incident.
  • Do NOT start a new on-call on Opsgenie — Atlassian is retiring it (EOL 2027-04-05; new sales ended 2025-06-04). Grafana OnCall OSS was also deprecated (folded into Grafana Cloud IRM). For a new setup use PagerDuty, incident.io, Better Stack, or Grafana Cloud IRM.

Verify it works

An untested alert is not an alert. Before you call monitoring "done," prove the wire end-to-end:

  1. Trigger a real synthetic failure (stop the service, or point a monitor at a forced-503 route).
  2. Confirm the page actually lands on a phone — not just "the rule exists in the UI."
  3. Let the ack timeout lapse and confirm escalation hops to the secondary.
  4. Restore, and confirm the resolve/all-clear notification fires too.

scripts/verify.sh enforces the structural half of this on your config: liveness split from readiness, at least one symptom/burn-rate alert with two windows, a runbook reference on every alert, and a banlist for Opsgenie-as-new-setup and homepage-only monitors. Run it in CI so config drift can't silently re-introduce a noisy or untested setup.

Anti-patterns

BadWhy it bitesDo instead
Monitor only the homepage // is 200 while checkout is broken — you learn from customerssynthetic check of the critical journey
Page on CPU/memory thresholdself-resolves, no user impact → alert fatiguepage on the symptom (error rate, latency, availability)
Alert on cause, not symptomcauses are noisy and ambiguousalert on what the user feels
Alert with no runbook linkthe paged human improvises at 3amevery alert links a 5-field runbook
Single-window threshold "by vibes"one spike pages; tuned by guessworkmulti-window multi-burn-rate against an SLO
Probe cascades every downstreamone slow dep fails the probe → restart loopcheap <100ms probe, deps in readiness with timeouts
Liveness checks the databaseslow DB → restart loop turns a blip into an outageliveness = process only; DB lives in readiness
Start new on-call on OpsgenieEOL 2027-04-05, dead endPagerDuty / incident.io / Better Stack / Grafana Cloud IRM
No secondary on-callprimary asleep/offline → page droppedprimary + secondary + manager escalation
No status pageinbound floods support during incidentspublic status page customers can self-serve
Never tested the alert"the rule exists" ≠ "the page lands"trigger a synthetic failure, confirm it pages + escalates
Page on warningstrains people to ignore pageswarning → ticket; only critical → page

References

  • references/tool-setup.md — Uptime Kuma docker-compose (rootless image, volume, port), HTTP + push-heartbeat + SSL/domain-expiry monitors, a synthetic multi-step journey, notification wiring (ntfy / Slack webhook / PagerDuty integration key), a UptimeRobot/Better Stack monitor JSON shape, and Go + FastAPI health-endpoint handlers.
  • references/burn-rate-and-oncall.md — full multi-window multi-burn-rate math, a copy-paste Prometheus-style alert rule with runbook_url, severity mapping, a sample escalation-policy YAML, and a fill-in runbook template.

Related: ../observability/SKILL.md (what the app emits), ../deployment/SKILL.md (release-gating healthcheck + rollback), ../error-handling/SKILL.md (degrade in code), ../domains-dns/SKILL.md (provision certs/DNS), ../scaling/SKILL.md (survive the load monitoring detected).

Signals

GitHub stars
82
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
monitoring-ericrisco
Source
github.com/ericrisco/rsc-harness