What to Instrument
A team handed a metrics library instruments what is easy: function call counts, cache hits, whatever the last incident touched. Six months later there are 200 metrics and still no way to say whether checkout is healthy. The Google SRE book's four golden signals are the antidote — latency, traffic, errors, saturation, measured at the service boundary. Four well-placed metrics on the checkout path answer more questions than 200 scattered ones.
This topic turns the instruments from topic 22 into a plan: which metrics the bookings service actually needs, in what order, and what each one will be asked during an incident.
The Four Signals on the Checkout Path
Latency is harborline_checkout_duration_seconds, already declared. Traffic is a counter, harborline_http_requests_total, read with the Chapter 4 pattern rate(harborline_http_requests_total[5m]) — now pointed at a metric the team owns instead of a host's. Errors are the 5xx fraction of that same counter family, one PromQL division away, which is why errors and traffic must live in the same metric.
Saturation is how full the limiting resources are: gunicorn workers busy, Postgres connections in use against db-01, Redis connections in use against cache-01. It is the least glamorous of the four and the one this chapter keeps circling back to.
Latency Honestly
A latency histogram lies unless failed requests are distinguishable from successful ones, so the histogram carries a status label. The failure mode is concrete: payments goes down, every checkout returns 500 in 20 ms, and a histogram that mixes them — or worse, records only successes — shows p95 improving while the service burns. Fast failures are the cheapest requests a service ever serves.
Bucket edges sit around the threshold that becomes the SLO in Chapter 10: 95% of checkouts under 500 ms. A histogram is blind between its edges, so precision is spent where the SLO question will be asked, not spread evenly from 5 ms to 10 s.
Boundaries First
One middleware on inbound HTTP instruments every route in a single move — latency, traffic, and errors for the whole service before anyone writes a bespoke metric. The route label comes from the framework's route template, never the raw path, for reasons topic 24 prices out in full.
@app.middleware("http")
async def observe_request(request, call_next):
start = time.perf_counter()
response = await call_next(request)
route = request.scope["route"].path # "/bookings/{id}"
labels = (route, request.method, response.status_code)
REQUESTS.labels(*labels).inc()
DURATION.labels(*labels).observe(time.perf_counter() - start)
return response
The middleware clocks the whole request, including time spent before the handler runs — exactly the component that grows when workers saturate. The same move repeats on the three outbound edges: one wrapper each around HTTP calls to payments, queries to db-01, and cache operations against cache-01, with the target as a bounded label. Interior functions wait; a boundary metric will point at the interior worth instrumenting when the time comes.
Saturation, the Signal Everyone Skips
For every bounded pool, two Gauges: connections in use beside the pool maximum, for both Postgres and Redis. Saturation is the only leading indicator among the four — latency and errors report damage already done, while a pool at 96% predicts it. Teams skip these gauges because nothing is on fire when they are written, which is precisely when they can still be written calmly.
The Saturday complaint has survived four chapters because nothing measured this. Host graphs looked healthy in Chapter 3 and PromQL got sharper in Chapter 4, but if the misbehaving resource lives inside bookings itself, it is invisible to every exporter. These pool gauges are the first instrumentation that can see inside the service at all — the proof has to wait for Chapter 8's traces.
Business Counters Ride Along
harborline_bookings_total and a counter of payment failures cost two lines each and answer the questions the business asks first. "Bookings per minute dropped 40% at 09:47" lands harder in an incident channel than any CPU graph, and it is the number the postmortem's impact section needs anyway. Add them the day the service is instrumented, not the day someone asks.
Four golden signals — latency, traffic, errors, saturation: the full checklist for a request-serving service. Use it for bookings, search, payments.
RED — Rate, Errors, Duration: the same list minus saturation. Fine as a floor, incomplete as a ceiling — it drops the one leading indicator.
USE — Utilization, Saturation, Errors: the lens for resources rather than services — hosts, disks, pools — and the frame node_exporter data already fits. Services get golden signals; resources get USE.
- Recording latency only for HTTP 200 responses — during a dependency outage the dashboard shows a splendid p95 while every user sees an error page, because the fast failures were excluded from the histogram.
- Starting the latency timer inside the route handler — time queued in gunicorn's backlog is invisible, and that is exactly the component that grows when workers saturate; the middleware must clock the whole request.
- Shipping traffic and latency but no saturation gauges — the Saturday complaint stays unanswerable at the service level, because a pool nearing its ceiling looks like nothing at all until it becomes latency.
- Counting errors in a separate metric family from traffic — a raw error count cannot become an error ratio, and 50 errors a minute means nothing without knowing whether that is 0.1% or 40% of requests.
- Instrumenting 30 interior functions before the boundaries — cardinality and maintenance cost with no view of what the caller experiences; boundary metrics are what tell you which interior deserves a metric.
- Instrument inbound HTTP with one middleware that emits latency, traffic, and errors for every route before writing any bespoke metric.
- Wrap each outbound dependency —
payments,db-01,cache-01— with its own duration histogram and error counter, labeled by target, so "slow" immediately decomposes into "slow where." - Expose an in-use Gauge and a maximum Gauge for every bounded resource pool, and read saturation as their ratio.
- Add a business counter for each event the company counts in money — bookings confirmed, payments failed — the day the service is instrumented, not the day someone asks.
http.server.request.duration and kin name these boundary metrics identically everywhereJVM Micrometer — auto-instruments the same server and client boundariesAPM Datadog / New Relic agents — automatic boundary instrumentation sold as the productKnowledge Check
Which Harborline metric carries the saturation signal for the bookings service?
- The pool in-use and maximum Gauges
- harborline_checkout_duration_seconds, since slowness means saturation
- rate(harborline_http_requests_total[5m])
- The 5xx fraction of the request counter
During a payments outage, every checkout fails in 20 ms. Why must those requests still land in the latency histogram, under their own status label?
- Excluding fast failures makes p95 improve exactly when the service is broken
- Prometheus rejects histograms that only contain successful requests
- Because the histogram is the only place errors can be counted at all
- Recording failures is free because they carry no label cost
A team adopts RED instead of the four golden signals. What did they give up?
- Saturation, the one leading indicator
- Latency, since RED only counts requests
- Nothing for services — RED is the resource-side checklist, so the loss only shows on hosts
- The ability to compute an error ratio
Why instrument the HTTP boundary before any interior function?
- One move covers all routes, and the results point at the interiors worth measuring
- Interior functions cannot technically hold Prometheus metrics
- Boundary metrics carry no labels, so they cost nothing
- Interior metrics answer the same questions, just with more effort
You got correct