Observability

SLO-Based Alerting and Metrics Instrumentation: Why Most Alerts Watch the Wrong Thing

A green dashboard and a working product are not the same claim, and most alerting setups quietly conflate them. This guide sets out seven principles for metrics instrumentation and SLO-based alerting that keep the two connected, so an alert firing actually means a user is about to feel something.

6 min read · 8 Principles · Cloud Platforms
Five teal cylindrical pillars of different heights arranged on a teal surface.

TL;DR

  • Every service entry point and every outbound dependency call needs its own request, error, and latency instrumentation.
  • Uncontrolled label cardinality breaks a metrics system faster than any bug the metrics were meant to catch.
  • A metric declared more than once, or registered per request instead of once at startup, produces data nobody can trust.
  • HTTP-level metrics can stay green while the business operation behind the request is quietly failing.
  • An alert threshold with no declared service level objective behind it is a number someone guessed.
  • The alerts that matter watch the user-facing path, not a single instance's CPU, and silence itself has to be treated as a failure mode.
  • Every team can individually instrument well and the organization can still end up with telemetry that doesn't compose, unless instrumentation goes through one shared interface.

What Metrics, SLOs, and Alerting Instrumentation Actually Protects

Instrumentation discipline is a narrow slice of what code governance covers, but it's the slice that decides whether an incident is caught by an alert or by a customer. A gap in coverage rarely shows up as a clean failure. It shows up as an on-call engineer reconstructing an outage from logs because the one endpoint that broke never had a latency histogram, or a revenue-critical failure that never paged because every HTTP-level metric stayed green, the same boundary-instrumentation problem our distributed tracing guide covers for request flow instead of metrics.

None of the seven principles below are about developer carelessness. They're about the fact that a metrics and alerting system degrades by default the moment cardinality, ownership, or objectives aren't enforced as a contract. Unbounded label cardinality and alert thresholds with no declared objective are exactly the gaps discipline alone can't close.

1. Every Entry Point and Dependency Needs Instruments

A service ships a new endpoint, or a call to a new downstream, and nobody adds instrumentation because it "just wraps an existing one." Weeks later, that exact endpoint is the one degrading during an incident, with no data. Every entry point needs a request counter, error counter, and latency histogram; every outbound call needs a latency histogram and outcome counter.

INSTRUMENTED: request and dependency metrics
REQUEST_LATENCY.labels(route="/orders").time()
REQUEST_ERRORS.labels(route="/orders").inc()

If a team can only measure a handful of things about a user-facing system, latency, traffic, errors, and saturation are the four worth measuring, applied at every boundary a request crosses.

2. Cardinality Kills Metrics Before Bugs Do

A team adds a user_id label to a histogram "for debugging." The metrics backend that tracked a few hundred time series now tracks millions, dashboard queries slow down organization-wide, and the monitoring system pages on-call for its own storage pressure. Never set a label to an identifier, a raw URL, or free text; use route templates and enumerated values.

ANTI-PATTERN: unbounded label values
requestDuration.WithLabelValues(r.URL.Path, userID)
CORRECTED: bounded, enumerable labels
requestDuration.WithLabelValues(routeTemplate, statusClass)

Every distinct label combination is its own time series with real memory and CPU cost. If a label's full set of values can't be enumerated before deploying, it doesn't belong in a label.

3. Metrics Declared Twice Are Already Wrong

Two files in the same service each define a counter for orders processed, with different names, because each was written at its call site without checking what already existed. Declare each metric once as a named constant, and register instruments once at module or startup scope, never inside a request handler or loop.

CORRECTED: declared once, referenced everywhere
const REQUEST_DURATION = new Histogram({ name: "req_duration_seconds" });
// module scope, not inside the handler

A metric name should mean the same thing everywhere it's read: impossible if it's declared and named independently at more than one call site.

Constructed fresh inside a handler, a histogram silently resets its buckets and loses history on every call, a bug that never throws and just quietly produces useless data.

4. Green Dashboards Can Hide Broken Revenue

A payment webhook silently starts rejecting a subset of transactions after a schema change. Every HTTP-level metric looks perfect, because the endpoint still returns 200; the failure happens one step downstream, in business logic the request metrics never see. Emit a dedicated counter for every revenue-bearing operation, distinct from HTTP metrics.

RULE OF THUMB
An HTTP 200 means the server didn't crash. It says nothing about
whether the order that request represented actually completed.

The team finds out from a support ticket, not a dashboard, days after the revenue impact started.

A dedicated business-outcome counter costs one more metric per critical operation. That's a trivial cost against a silent revenue failure hiding behind a green dashboard.

5. An Alert Without an Objective Is a Guess

An alert fires on "error rate above five percent," a threshold picked at setup with no record of why. Six months later nobody remembers if it's strict or lax. Declare alert rules as code, and express every threshold against a named service level objective in the repository, never as a bare literal.

ANTI-PATTERN: a bare, unexplained threshold
- alert: HighErrorRate
  expr: error_rate > 0.05
CORRECTED: threshold tied to a declared SLO
availability_slo: 0.999
expr: error_rate > (1 - availability_slo) * 14.4

An SLO measuring reliability the way customers actually experience it provides the highest-quality signal for deciding when an on-call engineer should respond, but only if the alert rule actually references it.

6. Alert on What Users Feel, Not Servers

An alert pages on a single instance's CPU crossing ninety percent while the load balancer already routed around it and no user noticed. A queue consumer falls behind for six hours with no lag alert, or a nightly job simply never runs. Alert on error rate, latency, and saturation of the user path, not a single instance's resources.

Every queue a service consumes needs a lag alert, and every scheduled job needs a completion heartbeat with an alert on its absence. A job that stops running entirely triggers no failure signal at all.

Node.js serves many clients with a small number of threads, so a callback that blocks one of them keeps that thread from handling any other client for as long as it stays blocked.

A short, severe burst of saturation can hurt users even while a longer-window utilization average looks completely fine.

7. Good Instrumentation Alone Doesn't Scale Across Teams

Team A instruments well by calling their monitoring vendor's client library directly. Team B, six months later, does the same with different naming and bucket conventions. Neither team did anything wrong, but the organization can't build a cross-service dashboard without a translation layer.

Emit metrics through the application's single shared instrumentation module, or a standard interface like OpenTelemetry, never by calling a vendor's client library directly from application code.

A shared naming scheme across a codebase and its libraries is what makes telemetry correlate and compose cleanly, and that only holds if every team routes through the same interface.

How Pandorian Enforces This Guide

Every principle above survives a design review. Few survive six months of independent team decisions, because instrumentation gaps aren't visible from inside any one pull request; they're visible only when an incident has no data behind it. Pandorian enforces this guide across three surfaces:

Caught at PR level

A new label bound to a raw identifier, a metric constructed inside a request handler, an alert threshold with no reference to a named SLO: each is a checkable code shape, flagged with the line, the rule, and a fix before it merges.

Full scans

New code discipline doesn't retroactively fix the endpoint from two years ago with no error counter, or the label quietly carrying a user ID. A full scan establishes the baseline across the whole codebase.

Scheduled scans

A new team's service brought online with its own vendor SDK calls, an alert threshold edited directly in a vendor console and never backported, a scheduled job with no heartbeat. Scheduled scans catch drift PR checks, by construction, never see.

A Taste of the Guideline Package

Four of the checks that ship in this package. Each runs at the PR level, on full or scheduled scans, or several at once.

RULE WHAT IT FLAGS
OBS_20314
A metric label bound to a raw user identifier, request ID, or an interpolated URL path instead of a route template.
OBS_22536
A metric instrument constructed inside a request handler, loop, or per-message function instead of module scope.
OBS_25869
An alert rule whose threshold is a bare numeric literal with no reference to a named service level objective.
OBS_29193
A scheduled job with no corresponding completion metric, heartbeat, or absence-based alert configured for it.

FAQ

What is a service level objective (SLO)?

+

A service level objective (SLO) is a specific, measurable reliability target for a service. For example, 99.9% of requests complete in under 300ms over a rolling 30-day window. It's the internal target a team commits to, distinct from an SLA, which is the externally promised, usually looser, contractual version of the same reliability measure.

What is an error budget?

+

An error budget is the amount of unreliability an SLO allows before it's considered breached. If the SLO is 99.9% availability, the error budget is the remaining 0.1%. Teams spend that budget on deploys, experiments, and planned maintenance; once it's exhausted, the usual response is to freeze risky changes until reliability recovers.

What is alert fatigue?

+

Alert fatigue is the state where on-call engineers become desensitized to pages because too many of them are low-value, noisy, or don't correspond to real user impact, leading people to acknowledge and dismiss alerts on reflex rather than investigate them. It's usually a symptom of alerting on resource thresholds or arbitrary literals instead of declared objectives.

Is it ever OK to alert directly on CPU or memory?

+

Yes, when that resource has a hard, known ceiling that causes an immediate user-facing failure the moment it's crossed: a JVM heap approaching an OOM kill, or a disk approaching 100% before a database refuses writes. Outside of ceilings like that, resource-level alerts should support diagnosis after a symptom-based alert has already fired, not replace it.

How do I tell if my alerts are tied to real SLOs or just arbitrary thresholds?

+

Open your alert rules and check whether each threshold references a named value declared elsewhere in the repository, or whether it's a bare number typed directly into the rule. If nobody can point to where "5%" or "200ms" came from, or explain what it's protecting, it's an arbitrary threshold wearing an SLO's clothes.

Can a linter or code review catch missing instrumentation?

+

Not reliably. Missing instrumentation is an absence, not a visible line of bad code, so a reviewer has to notice something that isn't there rather than flag something that is. Cardinality violations are worse: they only show up once real label values hit production, long after the code that introduced them was approved. Catching both requires scanning for the code shapes (unlabeled entry points, ad hoc metric declarations, unbounded label sources) across the whole codebase on a schedule, which is what full and scheduled scans are for.

Get the Guideline Package

You'll get this as a guideline MD file - hand it straight to your own coding agent or CI setup. Or skip the DIY step: give these same guidelines real enforcement superpowers, with PR checks and scans live on your repos, through Pandorian.

From Principles to Enforcement

Every rule in this guide is easy to agree with in a design review and easy to violate six sprints later, one locally-correct decision at a time. Pandorian turns boundary discipline like this into CI guardrails - scoped to the repos, teams or services you choose.