Where in a middleware chain do access logging and request metrics belong, and what does placing them too deep hide?
answer
- an observer wraps what it observes
- rejections must still be counted
- timer spans the whole chain
- label from matched route, after phase
- unmatched paths get one constant label
basics
~20 sAccess logging and request metrics belong at the outer edge of the chain, just inside the correlation-id hook, so they observe every request — including those an inner hook rejects — and time the whole pipeline, not just the handler.
solid answer
~40 sObservation hooks wrap everything they are supposed to observe, so they sit outermost, immediately inside the correlation-id hook. From there they see the outcome of every request: the ones a body guard refused, the ones a limiter answered `429`, the ones authentication answered `401`, and the ones that unwound into the error stage. Their timer also spans the whole chain, so the recorded duration includes body buffering, credential verification and limiter lookups, not just handler time. One tension pulls them inward: a route-labelled metric needs the matched route template, which is unknown on the way in. The resolution is placement plus phase — stay outermost, and read the matched route in the after phase, from what routing recorded on the request context, falling back to a single constant label when nothing matched.
go deeper
Know that one line per request is written after the response is settled, and that it belongs to a hook wrapping the whole chain rather than to the handler you happen to be writing.
Explain what a deep observer cannot see — rejected requests, pre-handler latency, error-stage outcomes — and how an outermost hook still obtains the matched route by reading it in the after phase.
Show the operational consequence: a traffic dip you cannot attribute, flat error rates during an incident, or a metrics backend degraded by path-labelled cardinality from scanner traffic.
Decide the standard: which labels exist at all, what the cardinality budget is, whether the edge or the app owns the canonical request count, and how the two are reconciled when they disagree.
## What these hooks are for An **access log** writes one record per request describing what happened: method, route, status, duration, size, and the correlation id. A **request metric** is the aggregate form of the same event: a counter of requests and a latency distribution, labelled with at least route and status. Both are *observers* — they change nothing about the request, and their only requirement is coverage. Coverage is a placement property. A middleware element can only observe what it wraps, so an observer's depth in the chain decides, exactly, which requests it can see and how much of each request's life it can measure. ## Why outermost Put them at the outer edge (just inside the correlation-id hook) and three things follow: - **Every request produces a record.** Including the ones no handler ever saw. - **The duration is the request's duration.** The timer starts before any inner hook and stops after the response has been produced, so it includes body buffering, credential verification, limiter lookups and serialization. - **The status is the status actually sent.** Because an outer observer's after phase runs once the response has been settled by everything inside it, including the error stage. ## What a too-deep observer hides Move the observers inside authentication and rate limiting, and the records they emit describe a service that is healthier and quieter than the real one: - rejections are invisible — a credential-stuffing wave answered `401`, or a client being throttled all afternoon, shows up as *less traffic*, not as a problem; - latency is understated, because the most expensive part of a slow request is often the part before the handler (a large body, a slow credential lookup); - error rates look flat, because failures raised by outer hooks never pass through the counter; - a traffic drop becomes ambiguous: you cannot tell whether clients stopped calling or the chain started refusing them. | Observer placed | Requests counted | Duration measured | |---|---|---| | Outermost (inside correlation id) | all, including every rejection | whole chain, in and out | | Inside auth and rate limiting | only those that got past both | chain minus the outer hooks | | Inside the handler | only those that reached the handler | handler body only | ## The tension: route labels A metric labelled by **raw path** is a well-known way to destroy a metrics backend: every identifier in a URL becomes its own time series, and a scanner probing random paths invents thousands more. The label must be the **route template** — the matched pattern, not the concrete path. But an outermost hook runs *before* routing, so on the way in it does not know the template. This pulls naive implementations inward, which trades away the coverage that justified the placement in the first place. The resolution is to keep the hook outermost and split its work across phases: 1. On the way in: start the timer, nothing else. 2. Delegate. 3. On the way out: read the matched route that routing recorded on the request context, read the final status, and emit. When nothing matched, or the request never reached routing, emit a single constant label — one series for all unmatched traffic — rather than the path. That keeps cardinality bounded while still counting the request. ## Practical rules that come out of this - **One access-log line per request, written in the after phase**, not one on the way in and one on the way out; a line written on the way in cannot carry status or duration. - **Keep the observers cheap and non-failing.** An observer that throws turns a served request into an error; if the log destination is unavailable, drop the record, do not drop the request. - **Do not put anything that can reject a request outside them.** Whatever sits outside the access log is, by construction, unlogged. - **Label sparingly.** Route, method, status class. Anything caller-controlled is a cardinality risk. - **Metrics and logs at the same depth.** If the two observers sit at different depths, their totals will disagree during exactly the incident where you need them to agree. ## How to answer this in an interview Say the principle first — *an observer must wrap what it observes* — then name the categories of request that a deep observer loses, then show that you know the route-label tension and resolve it with the after phase rather than by moving the hook. That sequence demonstrates you have reasoned about the chain rather than copied a known-good ordering.
- What is wrong with labelling a request metric by the raw request path?Every distinct concrete path becomes its own time series, so identifiers in the URL multiply the series count without bound, and unmatched probes from scanners add thousands more. Use the matched route template, and a single constant label for traffic that matched no route.
- If the access-log hook is outermost, how does it record a route for a request that never matched one?It reads the matched route from the request context in its after phase and finds nothing there, so it writes a constant marker instead of the path. The line still records method, status, duration and correlation id, which is what makes a burst of unmatched requests visible at all.
- Should the access-log hook ever short-circuit or fail a request?No. It is an observer, so it must be effectively non-failing: if its destination is unavailable it should drop the record rather than the request. A logging failure that propagates converts a successfully served response into an error, which is a much worse outcome than a missing line.
saying these in an interview costs you the question
- Times only the handler and calls the result request latency
- Puts the access log inside authentication, so rejections vanish
- Labels metrics with the raw path including identifiers
- Writes the access-log line on the way in, before the status exists
- Lets a failing log destination turn a served request into an error