Monitoring, Logging, and Observability Questions
Understanding running systems through their signals. Covers metrics, logs, and traces, instrumentation, dashboards, alerting design, and log analysis and correlation for debugging production. Emphasizes designing observability so problems are detectable and diagnosable before users are affected.
Your 5xx rate is climbing, but your logs show hardly any error messages. Walk through how you'd combine metrics, targeted logging, tracing, and exemplars to actually narrow down the root cause without just cranking log verbosity everywhere.
Sample Answer
Use metrics to localize which dimension (service, endpoint, region, deployment) the 5xxs are actually concentrated in before touching logging at all, then use exemplars to jump straight from that metric spike to a handful of real trace IDs, and only then turn up structured logging narrowly, scoped to the localized service and time window rather than everywhere. The fact that logs show almost nothing is itself a clue worth taking seriously: it often means the 5xx isn't being generated inside application code at all (a gateway/load-balancer timeout, a connection reset, a resource limit hit before the request ever reached a log statement), which is exactly the case where blanket log-level increases wouldn't have helped anyway.
Framework
1. Localize with metrics first. Break the 5xx rate down by every dimension you have: sum(rate(5xx[5m])) by (service, region, deployment_version). This is cheap (metrics are already low-cardinality and aggregated) and usually narrows a fleet-wide-looking problem down to one service or one deployment within minutes.
2. Attach exemplars to the metric, not just the count. An exemplar is a sampled trace ID attached to a specific bucket of a histogram or counter, so a spike in the 5xx counter or the p99 latency bucket comes with a direct link to one or more real traces that landed in that bucket. This turns "something is wrong in service X" into "here is an actual example request that failed," without a log search.
3. Follow the exemplar trace to see where the request actually died. If the trace shows the request never reaching application code (e.g. the last span is at a load balancer or a client-side connection attempt with no corresponding server span), that itself is the finding: the error is being generated upstream of the app (timeout, connection refusal, TLS/handshake failure), which explains why app-level logs show nothing, because the app never got the request or never got to finish handling it.
4. Only now, scope up logging narrowly. If the trace does show the request reaching the app and failing there, raise log verbosity (or sampling rate) for that specific service and route, for a bounded time window, rather than turning up verbosity fleet-wide, which mostly adds noise and ingestion cost without adding signal for a problem you've already localized.
Worked example
Say a 10-minute window shows 600,000 total requests split evenly across three backend services (A, B, C at 200,000 each), and 480 total 5xxs over that window, an aggregate rate of:
600,000480=0.08%That 0.08% aggregate is easy to dismiss as noise. Breaking it down by service:
| Service | 5xx count | Rate | Share of all 5xx |
|---|---|---|---|
| A | 40 | 0.020% | 8.3% |
| B | 40 | 0.020% | 8.3% |
| C | 400 | 0.200% | 83.3% |
Service C accounts for 83.3% of all 5xxs, and its own rate is 10x the other two services' baseline rate. That's the localization: the aggregate number was hiding a strong, concentrated signal in one service, exactly what the by-dimension breakdown surfaces and a single fleet-wide number can't.
From here, an exemplar attached to C's 5xx counter points at real failing traces from C specifically, and following one of those traces (rather than searching C's logs blind) shows whether the failure originates in C's own code or in whatever C is calling.
Trade-offs and pitfalls
- Turning up DEBUG logging everywhere, which the scenario explicitly warns against, is expensive twice over: it floods the log pipeline with noise that makes the real signal harder to find, and under genuine resource pressure (which could be part of what's causing the 5xxs), the extra logging load can itself make things worse.
- Exemplars are only as useful as the histogram bucket boundaries they're attached to: a coarsely-bucketed latency histogram might not have a bucket edge anywhere near the actual latency of the failing requests, so the exemplar trace may not represent the specific failure mode you're chasing. Bucket boundaries matter and are worth reviewing before an incident, not during one.
- A gateway or load balancer that returns its own 5xx (on an upstream timeout, say) without the request ever reaching application code is a classic blind spot: application logs, application traces, and application metrics all look clean, because from the app's point of view, nothing happened. Catching this requires looking at the load balancer's own metrics/logs, which is exactly why "logs show hardly any error messages" is a clue pointing outside the app, not a dead end.
- High-cardinality tags (a specific customer ID, a specific request ID) should never be added directly to metric labels to make localization "more precise," since that turns a cheap aggregated query into an expensive, potentially cluster-destabilizing one; that level of detail belongs in the sampled traces and logs, not in the metric's label set.
Have you managed your dashboards, alert rules, and metric definitions as code, checked into version control the same way as application code? What does that actually buy you, and what's harder about it compared to clicking around in a UI?
Sample Answer
Direct answer
Yes, and the honest answer to what it buys you is the same thing version control buys application code: review before it ships, a diff when something changes, and the ability to revert a bad alert threshold as fast as you'd revert a bad deploy. What's harder is that a GUI lets you make a quick, well-intentioned tweak in seconds, and observability-as-code trades that speed for review overhead, which is worth it for anything that pages someone, but can feel like friction for a genuinely trivial change.
Structured elaboration
What managing dashboards and alerts as code actually buys you
| Property | UI-managed | Managed as code |
|---|---|---|
| Reviewability | No diff, no approval step by default | PR review before a change to a paging threshold ships |
| Drift detection | Silent: a manual click can diverge from what anyone documented | Git is the source of truth; drift is detectable by diffing against deployed state |
| Rollback | Manual, relies on someone remembering the prior value | Revert the commit, same as reverting application code |
| Onboarding consistency | Every new service gets configured slightly differently by whoever set it up | A shared template/module ensures every service starts from the same baseline |
| CI validation | None | Linting (for example promtool for Prometheus rules) can catch a syntactically broken alert rule before it ever reaches production |
What's harder about it
The as-code tooling has its own learning curve (a YAML or JSONNET dialect on top of whatever the underlying platform's own query language already is), and some dashboard-heavy workflows, like eyeballing a panel while dragging its boundaries to get the layout right, are genuinely faster in a GUI. Not every vendor supports full-fidelity export/import either, so "everything as code" sometimes means committing to a subset of features the export format actually preserves.
Worked example
A concrete before/after: an engineer edits an alert rule's PromQL expression and, in the process, introduces an unbalanced parenthesis. In a GUI-only workflow, that error might not surface until the rule silently fails to evaluate in production, no clear symptom, just an alert that never fires again. In an as-code workflow, the same typo is caught by a CI lint step (promtool check rules) that parses the file and fails the pipeline before the change merges, so the broken rule never reaches production at all. The value isn't the specific tool, it's that a syntax error becomes a blocked PR instead of a silently dead alert discovered weeks later during an incident it should have caught.
Trade-offs and pitfalls
A passing lint check only proves the syntax is valid, not that the threshold or query logic is actually correct, so treat CI validation as catching one class of bug (the code doesn't parse) and pair it with an actual review of the logic, not a substitute for that review. Shared dashboard-as-code modules can also create merge conflicts when multiple teams edit the same file, which is a real cost that a GUI never has, and worth designing around (per-team files, clear ownership boundaries) rather than discovering the hard way. Finally, don't over-apply the discipline: a genuinely low-stakes internal debugging dashboard that nobody pages off of doesn't need the same review overhead as a paging alert rule, and treating every change identically just trains people to route around the process.
Design an automated system that reacts to error-budget burn: it should be able to roll back a feature flag, shift traffic away from a bad region, or freeze deploys entirely, depending on how fast the budget is burning. What safety checks and manual overrides would you build in, and how would you test it without risking a bad automated rollback in production?
Sample Answer
Direct answer: Build a burn-rate-driven policy engine, not a single "if error budget hits zero, do X" rule: it watches how fast the budget is burning, tiers its responses by severity and blast radius (a safe, automatic action for well-understood, reversible situations; a human-approved action for anything with wide blast radius), and every automated action must be independently reversible and testable in isolation before it's ever allowed to run against real traffic.
Structured elaboration
Two-tier action model
- Tier A (automated, safe): actions that are cheap to reverse and scoped narrowly, rolling back a single feature flag, shifting a small percentage of traffic away from a degraded region. These fire automatically once burn rate crosses a defined threshold.
- Tier B (automated proposal, human approval required): actions with wide blast radius, freezing all deploys org-wide, failing over an entire region. The system proposes the action with its reasoning and evidence, but a human clicks approve.
Safety checks before any automated action
- Verify the rollback target actually exists and was recently healthy (don't roll back to a flag state that was itself broken).
- Check a dependency graph so a feature-flag rollback or traffic shift doesn't break something else that depends on the current state.
- Rate-limit the automation itself: one automated action per service per cooldown window, so a flapping signal can't trigger a rollback loop.
- Every action requires a kill switch: one command that halts all in-flight and future automated actions immediately, checked before every state transition, not just at startup.
Testing without risking a bad automated rollback in production
- Run the exact policy engine against a staging environment with fault injection (inject latency, error rate) and confirm it fires the correct tier at the correct threshold, with no live traffic involved.
- Roll out the automation itself progressively: shadow mode first (it decides what it would do and logs the decision, but doesn't act), then Tier A actions only against a small percentage of traffic or a low-risk service, then expand.
- Chaos-test the automation's failure modes specifically: what happens if the actuator (feature-flag API, load balancer) is unreachable when the policy tries to act? The system should fail toward "alert a human," never toward "retry aggressively" or "silently do nothing."
Worked example (architecture)
flowchart TD
A["Metrics collector<br/>Prometheus / Datadog"] --> B["SLO evaluator<br/>burn-rate rules"]
B --> C{"Burn rate<br/>threshold?"}
C -->|below warning| D["No action, log burn rate"]
C -->|warning| E["Notify on-call + owner"]
C -->|critical, Tier A| F["Automated safe action<br/>flag rollback / traffic shift"]
C -->|critical, Tier B| G["Propose action,<br/>require human approval"]
F --> H["Actuator<br/>feature-flag API / LB / gateway"]
G -->|approved| H
H --> I["Verify: smoke test +<br/>post-action SLI check"]
I -->|recovered| J["Log to audit trail,<br/>close incident"]
I -->|not recovered| K["Escalate to Tier B<br/>/ page human"]
K --> G
This makes the tiering concrete: burn rate below a warning threshold does nothing but log, a warning-level burn notifies without acting, and only a critical burn rate triggers an action, split into Tier A (auto-execute) and Tier B (propose, wait for approval). Every path converges on a verification step, so the system checks its own work rather than assuming the action succeeded.
Trade-offs & pitfalls
- More automation reduces time-to-mitigate but increases blast radius if the policy engine itself has a bug; that's exactly why Tier A is scoped to narrow, reversible actions and Tier B requires a human for anything wider.
- A automation that fires too eagerly (low threshold, short window) risks flapping, an action, then a rollback of that action, then re-triggering. The cooldown and rate-limit above exist specifically to prevent this, and any burn-rate threshold should be validated against real historical burn-rate noise before being trusted to act automatically.
- Testing this system is unusually hard because the dangerous failure mode (a bad automated rollback under real production load) is exactly what you can't safely rehearse in production. Shadow mode plus staged rollout of the automation itself is the practical answer, accept slower confidence-building in exchange for never letting an untested policy touch live traffic.
- CI/CD gating integration (blocking new deploys while budget is critically low, on top of the runtime mitigations above) closes a related but separate gap: the runtime system reacts to what's already live, a deploy gate prevents making things worse while it's still burning.
Your team is choosing between a hosted observability platform and a self-hosted open-source stack for a growing company with a small operations team. Walk through the trade-offs you'd weigh, things like cost, operational overhead, feature completeness, and vendor lock-in, and what would tip your recommendation one way or the other.
Sample Answer
Direct answer
For a growing company with a small operations team, default to a hosted platform unless one team already has strong operational muscle for a specific piece of the stack (most often metrics, via Prometheus). The variable that actually decides this is engineer-hours available for care and feeding, not sticker price: a self-hosted stack usually undercuts hosted pricing on paper, but that gap closes or reverses once you count the SRE time spent on cluster capacity, upgrades, and retention tuning.
Decision framework
| Dimension | Hosted (Datadog, New Relic, Grafana Cloud style) | Self-hosted OSS (Prometheus + Loki + Grafana + Tempo) |
|---|---|---|
| Upfront cost | Low, pay-as-you-ingest | Low licensing, but infra plus engineer time is a real cost |
| Ongoing cost at scale | Grows fast with hosts/ingestion, can dominate the infra bill | Grows with storage/compute you already control, more linear |
| Operational overhead | Near zero: vendor handles scaling, upgrades, HA | Real: cluster sizing, upgrades, backup/restore, on-call for the observability stack itself |
| Feature completeness | Turnkey APM, anomaly detection, log parsing UIs, SLO tooling out of the box | Comparable core signal collection, but polish (auto root-cause, ML anomaly detection) usually lags or needs extra tooling |
| Vendor lock-in | Real: proprietary query language, dashboards don't port cleanly | Low: OpenTelemetry, PromQL, and LogQL are portable across backends |
| Multi-tenant RBAC (role-based access control: who can see/query which logs) / PII redaction (log aggregation) | Usually built in (SSO, field-level masking) as a paid-tier feature | You build and maintain it yourself (access policies at the query layer, redaction at the log shipper) |
When to tip toward each:
- Hosted: team is small, time-to-value matters more than unit cost, nobody owns the observability stack as their primary job, or you need APM/anomaly detection you don't want to build yourself.
- Self-hosted: you already run Kubernetes at scale with a platform team that can treat observability as just another workload, data residency or compliance forces on-prem storage, or ingestion volume is high enough that hosted per-GB pricing becomes the single largest line item in the infra budget.
- Hybrid: a common middle path is hosted APM and log search (where turnkey correlation and UI matter most) paired with a self-hosted Prometheus for metrics you already understand and want fast, cheap, high-resolution queries on. This works because metrics are the cheapest and most mechanical piece to self-host, while tracing and log search UX is where vendors differentiate most. Re-evaluate the split as the team and ingestion volume grow.
Worked example
The crossover point between the two options can be derived, not guessed, once you write cost as a function of ingestion volume.
Let G be GB/day ingested. Model hosted cost as a per-GB ingestion rate, and self-hosted cost as a fixed infra floor plus a much cheaper per-GB storage rate plus ongoing engineer-hour labor:
Gbreak−even=30×(rhosted−rstorage)Cinfra+H⋅RlaborUsing illustrative, explicitly-assumed rates (not any specific vendor's current published price, since list prices change): hosted ingestion rate rhosted=$0.10/GB, self-hosted object-storage rate rstorage=$0.02/GB, fixed self-hosted compute floor Cinfra=$300/month, and H=4 hours/month of engineer time at a loaded rate Rlabor=$150/hour:
Gbreak−even=30×(0.10−0.02)300+4×150=2.4900=375 GB/dayUnder these assumptions, below roughly 375 GB/day of ingestion, hosted comes out cheaper on pure dollar terms even before counting the small-team operational risk. Above it, self-hosting's lower per-GB rate starts to outweigh the fixed compute and labor floor. The point isn't the exact number, it's that a candidate should reason in terms of where the cost curves cross, not assert a universal winner.
Trade-offs and pitfalls
- Sunk-cost fallacy: don't keep self-hosting because it was already built, once engineer time becomes scarce elsewhere the calculus can flip.
- Hidden costs of hosted: egress and API costs, per-seat pricing for dashboard users, and price increases once you're locked in and migration feels expensive.
- Hidden costs of self-hosted: the observability stack becomes a second production system that itself needs monitoring, on-call, and capacity planning. If it goes down during an incident, you're debugging blind.
- Migration cost is asymmetric: moving off a hosted platform later means rebuilding dashboards and alerts in a new query language; moving off self-hosted OSS is comparatively easier since OpenTelemetry-based data is portable to almost any backend.
- Common wrong turn: picking self-hosted purely because a spreadsheet says it's cheaper while ignoring engineer-hours, then rediscovering the true cost six months later at the first major version upgrade.
You've got an anomaly-detection alert on a per-entity rate, think per-customer or per-tenant request volume, and it fires constantly for your smallest customers while staying quiet for your largest ones. How would you redesign the detection so it's sensitive to real anomalies at both ends of that size range?
Sample Answer
Direct answer
A flat percentage threshold ("alert if volume drops 30%") is the bug: it treats a small customer's normal statistical noise as equally meaningful as a large customer's genuine anomaly, when the two have very different natural variability. The fix is to scale the sensitivity to the expected variance at each customer's volume, so the alert reacts to how surprising a change is, not just how big it looks as a percentage.
Structured elaboration
Why static percentage thresholds fail across a size range
Count-based data like requests-per-hour naturally fluctuates more, in relative terms, the smaller the volume is. A customer normally sending 50 requests/hour swinging to 35 in a given hour is well within ordinary noise; a customer normally sending 100,000 requests/hour dropping by the same 30% is a massive, almost certainly real event. A single relative threshold can't tell those apart, because it was never designed to account for volume-dependent variance in the first place.
Redesigning the detection
- Scale sensitivity to expected variance, not to a flat percentage. For count data, a common and reasonably good approximation is that variance is close to the mean (a Poisson-like assumption), which means the standard deviation grows with the square root of volume, not linearly with it. Comparing a change in units of that standard deviation (a z-score) instead of raw percentage automatically adapts sensitivity to volume.
- Add a minimum absolute floor. Even with a z-score approach, an extremely small customer going from 1 event to 0 can register as a huge relative or statistical swing that isn't actually meaningful; require both a statistical threshold AND a minimum absolute change before alerting.
- Require persistence. A single anomalous window can be a fluke; requiring the anomaly to hold across a couple of consecutive windows filters out one-off noise for both ends of the size range.
Handling non-Poisson reality (beyond what most interviews require)
Real traffic has diurnal and weekly seasonality and is usually over-dispersed relative to a pure Poisson model (its true variance is larger than its mean), so a production system typically layers a seasonal baseline or an EWMA-smoothed rolling variance, or a more robust statistic like the median absolute deviation, on top of the basic idea rather than using raw Poisson variance directly. The core insight, scaling sensitivity to expected variance instead of using a flat percentage, is the part worth leading with; the seasonal/robust refinements are worth mentioning as the natural next step, not the starting point.
Worked example
Assume request counts are approximately Poisson-distributed, so standard deviation is approximately the square root of the mean, and compare a small customer (mean 50 requests/hour) against a large one (mean 100,000 requests/hour) under both the old 30% static threshold and a redesigned z-score-based one.
σ≈μ,z=σμ−xA 30% drop for the small customer (50 to 35) versus the large customer (100,000 to 70,000), measured in standard deviations:
zsmall=500.3×50=7.0715≈2.12,zlarge=100,0000.3×100,000=316.230,000≈94.9That gap, 2.12 versus 94.9 standard deviations for the same "30% drop," is exactly the miscalibration in the question: for the small customer, a 30% swing is only mildly unusual (roughly a 2-sigma event, which happens by chance often enough to explain the constant noisy alerts), while for the large customer it's a statistical impossibility under normal variation, meaning a real 30%-threshold rule was tuned to something that's noise-level for small customers and would almost never even get the chance to fire for large ones since a genuinely damaging but smaller drop wouldn't reach 30%.
Now redesign using a fixed z-score threshold of 4 (a much rarer, roughly-equally-surprising event at both ends) and translate it back into request counts:
xsmall=μsmall−4σsmall=50−4(7.07)≈21.7 xlarge=μlarge−4σlarge=100,000−4(316.2)≈98,735(a 1.27% drop)So the redesigned rule fires for the small customer only once volume drops below about 22 (a genuinely rare event, not routine noise), and for the large customer it fires on a drop of just 1.27%, far more sensitive than the old flat 30% rule ever was, and appropriately so, since a 1.27% drop at that volume is exactly as statistically surprising as the small customer's much larger relative swing.
Trade-offs and pitfalls
The minimum absolute floor is not optional even after adding the statistical scaling: a customer going from 2 events to 0 can register as an enormous z-score purely because the Poisson approximation breaks down at very low counts, so pair the statistical threshold with a hard floor like "don't alert unless the absolute drop is at least N events." A second pitfall is trusting the pure Poisson assumption too far into production: real traffic has daily/weekly seasonality that a naive Poisson model doesn't capture, and treating a predictable Monday-morning dip as anomalous will just recreate the noise problem in a new form, so the production version needs a seasonal or rolling baseline underneath the same z-score logic. Finally, explainability matters more here than raw model sophistication; an on-call engineer needs to be able to look at why an alert fired and reconstruct the reasoning, which favors a transparent statistical approach like this over an opaque ML anomaly detector the team can't debug at 3am.
Unlock Full Question Bank
Get access to all Monitoring, Logging, and Observability interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.