Monitoring and Observability: Knowing Your App Is Alive
The worst way to learn your site is down is from a customer's message. The second worst is from a dashboard you opened out of habit. Good monitoring is boring by design: it tells you something is wrong before anyone else notices, and it gives you enough context to fix it without a scavenger hunt. Here's how we set that up for the apps we run.
Start with health checks, not dashboards
Before any fancy metrics, your app needs a simple answer to one question: are you alive? A /health endpoint that checks the essentials — database connection, cache, queue worker — and returns a fast 200 or 503 is the foundation of everything else.
// Laravel — a health endpoint that actually checks something
Route::get('/health', function () {
try {
DB::select('SELECT 1');
} catch (Throwable) {
return response('db down', 503);
}
return response('ok');
});
Point an external uptime checker at it (a free service or a cron job on another server — never the same box you're checking). External checks catch what internal ones can't: DNS failures, dead network paths, and a server that's up but unreachable.
Logs are for debugging, metrics are for trends
These two get confused constantly. Logs answer what happened — a stack trace, a failed payment, a slow request. Metrics answer how is it going — request rate, error rate, latency percentiles, queue depth.
For logs, the rule is: structured or nothing. JSON lines with a timestamp, level, and request ID beat free-form text every time, because you can grep, filter, and correlate them.
{"time":"2026-09-08T08:00:01Z","level":"error","request_id":"abc123","message":"payment failed","provider":"stripe","code":"card_declined"}
A request ID passed from the frontend through the backend into the logs is the single most useful debugging tool you can add — it turns "something broke" into "this exact request broke, here's every step it took."
For metrics, don't build a dashboard for everything. Start with five numbers: request rate, error rate, p95 latency, queue length, and free disk space. If one of those looks wrong, you have a problem. Everything else is noise until you've actually needed it.
Alert on symptoms, not causes
The classic mistake: alerting on CPU at 90% and waking up at 3 a.m. for a spike that resolved itself. CPU is a cause, not a symptom. The symptom is "users can't check out" or "the API is returning 500s."
Alert on what the customer experiences: error rate above a threshold for five minutes, p95 latency tripling, queue backing up, disk filling. Those are the things that cost you money. When an alert fires, you should already know roughly what to look at — that's the difference between an alert and a page.
The minimum viable setup
You don't need a commercial observability platform on day one. A pragmatic stack looks like this:
- Uptime check on
/healthfrom an external service — free tier is fine - Structured logs shipped to a central place you can search (even a simple log aggregator on your VPS)
- A handful of metrics — error rate and latency are the two that matter most
- One alert channel — email or a chat webhook, not five different tools
- A runbook — one page that says: "if this alert fires, check these three things first"
The runbook is the part everyone skips and everyone regrets skipping at 3 a.m. Write it while you're calm, not while the site is down.
What we deliberately don't do
We don't monitor everything, and you shouldn't either. No alert for every log line, no dashboard with forty panels nobody reads, no paging for issues that can wait until morning. Every alert you add is a cost — it trains you to ignore the tool that's supposed to save you. A small, sharp setup that fires rarely and means something beats a comprehensive one that fires constantly and means nothing.
Monitoring done right is quiet. The goal isn't beautiful dashboards — it's that when something breaks, you know first, you know what to look at, and you fix it before your customers ever notice.