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.
An alert keeps firing and clearing repeatedly for what's really one ongoing issue. How would you deal with the flapping without hiding the fact that there's a real, persistent problem underneath it?
Sample Answer
Direct answer: Flapping means the underlying issue is real and ongoing, it just isn't crossing the alert condition cleanly. Fix it with hysteresis (a different threshold to clear an alert than to raise it) and grouping (collapse the repeated fire/clear events into one ongoing incident), not by silencing the alert, silencing would hide the exact persistent problem the question is asking you not to hide.
Structured elaboration
| Technique | What it does | Trade-off |
|---|---|---|
| Hysteresis | Use two thresholds: fire at a higher bar, clear at a lower one, so a metric oscillating right around a single line doesn't flap | Delays resolution slightly, the alert stays open a bit longer than a single-threshold rule would |
| Debounce / persistence window | Require the condition to hold continuously for N minutes before firing (and before clearing) | Increases time-to-detect by up to the window length |
| Stateful grouping | Collapse repeated fire/clear cycles for the same underlying signal into one open incident instead of one notification per flap | Requires a stable identity for "the same issue" (a fingerprint), or unrelated issues can get incorrectly merged |
| Severity escalation over time | Start at a lower severity, escalate if the flapping (or the underlying condition) persists past a duration threshold | Adds state-machine complexity; a badly tuned escalation delay can under- or over-react |
Applying these without hiding the real problem
- Hysteresis and debounce reduce the number of individual fire/clear notifications, but the underlying incident record should stay open and visible the entire time the condition keeps recurring, not just during the moments it's actively firing.
- Grouping should merge notifications, not merge away the evidence: the incident should retain a count of how many times it flapped and the full timestamp history, so on-call can see "this has fired 14 times in the last hour" rather than a single anonymous alert with no memory of the pattern.
- Escalate severity based on persistence, not suppress it: a condition that's been flapping for an hour is a stronger signal that something is wrong, not a weaker one, and the escalation should reflect that (raise severity or broaden the audience) rather than quietly muting it because "it keeps clearing on its own."
Worked example
A disk-usage metric oscillates between 78% and 82% because of periodic log rotation, with a single static threshold at 80%. With no hysteresis, this fires and clears repeatedly as the metric crosses 80% each cycle. Applying hysteresis with a fire threshold of 85% and a clear threshold of 75%:
- The metric's actual range (78% to 82%) never touches either the fire line (85%) or, once fired, the clear line (75%) inconsistently, it stays comfortably inside the dead zone between the two thresholds and doesn't flap.
- If disk usage genuinely climbs past 85% (a real leak, not the periodic rotation pattern), it fires once and stays fired until it drops all the way back under 75%, giving one clean incident instead of a stream of fire/clear notifications for the same underlying trend.
This is a case where the "fix" isn't detecting the flapping better, it's recognizing that a single threshold was never the right model for a metric with a normal 78 to 82% oscillation range, and setting the dead zone wide enough to cover that normal range is what actually solves it.
Trade-offs & pitfalls
- Every one of these techniques trades detection speed for noise reduction. A condition that genuinely needs an immediate page (imminent data loss, a security event) should not be debounced or given wide hysteresis, the noise-reduction techniques above are for conditions where a few minutes of delay is an acceptable cost for not paging someone six times an hour.
- Grouping by a fingerprint that's too broad merges genuinely unrelated issues into one incident (masking that there are actually two separate problems); too narrow, and it fails to group the flaps it was meant to collapse. The fingerprint needs to key on the actual root-cause dimension (host, service, error signature), not just the alert name.
- A hysteresis dead zone set too wide delays real detection meaningfully; too narrow, and it doesn't solve the flapping at all. The right width comes from looking at the metric's actual normal oscillation range (as in the worked example), not from a generic default.
A recent deploy burned through a big chunk of your error budget for the month. Walk me through how you'd structure the postmortem: what data you'd pull together, how you'd explain the impact, and what you'd walk away with to prevent it happening again.
Sample Answer
Direct answer: Structure the postmortem around a timeline, not a blame narrative: pull the deployment timestamp, overlay it against the SLI dashboard and the error-budget burn, quantify exactly how much budget the incident consumed, then work backward from the first observed symptom to root cause using traces and logs. The walk-away artifact is a concrete, owned action list, not a general "be more careful" resolution.
Structured elaboration
Data to pull together
- Timeline anchor: the deployment or config-change timestamp, this is almost always where the investigation starts, since most error-budget-burning incidents correlate tightly with a recent change.
- SLO dashboard: the SLI's value before, during, and after the incident, plus the error-budget burn-rate chart so the team can see not just "it broke" but "how fast it was burning."
- Errors by type: which endpoints, which status codes, which exception types spiked, this narrows root cause from "something broke" to "this specific code path broke."
- Traces and logs: a handful of representative failing traces (not all of them, a representative sample is enough to see the pattern) correlated with structured logs via trace ID.
- Resource and dependency metrics: CPU/memory/GC on the affected hosts, and latency/error rate on anything downstream, to distinguish a code bug from a resource exhaustion or dependency failure.
How to explain the impact
- Quantify budget consumed as a fraction of the period's total budget (see worked example), not just "the SLO dropped." A number stakeholders can compare to "how much budget is left this month" is far more actionable than a description of the dashboard dip.
- State the user-facing impact in concrete terms: percentage of requests affected, estimated number of unique users or sessions, and duration of impact.
What you walk away with
- A root cause stated at the mechanism level ("the new code path opened a DB connection per request without pooling, exhausting the connection pool under peak load"), not at the symptom level ("errors increased").
- A small number of owned, dated action items, tightening canary criteria, adding a specific alert that would have caught this faster, a pre-deploy check that would have caught the bug in the first place. Each with a named owner and a due date, reviewed at a fixed checkpoint (commonly one week out) to confirm they actually landed.
- A decision on rollback vs patch-forward for this specific incident, documented with the reasoning, so the next similar incident has a precedent to reference instead of re-litigating the decision from scratch.
Worked example
Assume the service has a monthly correctness error budget of 2,000 failed requests (at 99.9% SLO over 2,000,000 monthly requests, following the same derivation as the SLI/SLO worked example above). A bad deploy is live for 45 minutes before rollback, during which the error rate on this service jumped from a normal 0.05% to 8%, with average traffic of 400 requests/minute during that window.
Failed requests during the incident:
400×45×0.08=1,440 failed requestsBaseline failures that would have happened anyway in that window (at the normal 0.05% rate) don't count against the incident's attributable impact:
400×45×0.0005=9 failed requestsAttributable incident impact:
1,440−9=1,431 failed requestsAs a fraction of the monthly budget:
2,0001,431=0.7155→about 72% of the entire month’s error budget consumed in 45 minutesThat single number, 72% of the month's budget gone in under an hour, is what belongs at the top of the postmortem's impact section: it makes the severity legible to anyone reading the doc, engineer or not, without them needing to interpret a dashboard.
Trade-offs & pitfalls
- A postmortem that stops at "we rolled back and errors went away" without root-causing the mechanism leaves the door open for the same bug to resurface in a different code path. Push to the mechanism, not just the symptom.
- Action items with no owner or due date are the single most common reason postmortem follow-through fails; the one-week checkpoint above exists specifically to catch that before it becomes a pattern.
- The same structure applies when the trigger is a sustained SLO violation accumulating over several days rather than one bad deploy, the difference is that the timeline anchor isn't a single deploy event, it's a slower-moving cause (a capacity ceiling being approached, a dependency degrading gradually), so the investigation should explicitly check "did this correlate with a change, or was there no single change and the system just outgrew its current capacity."
You're onboarding observability for a brand-new service. What do you instrument first, metrics, traces, or logs, and why? How do you keep performance and cardinality overhead in check while still leaving yourself enough to debug with later?
Sample Answer
Direct answer: Start with metrics: they are the cheapest signal to collect, they are what your alerts and SLOs are built on, and you need them from hour one to know the service is even healthy. Add structured logs next, since that is where root-cause detail lives once an alert fires. Bring in distributed traces as the service starts talking to other services, because that is when "which hop was slow" becomes the actual question. All three should exist before the service takes real traffic, this is a sequencing preference within a short onboarding window, not "ship logs-only for a month."
Structured elaboration
Metrics first: what to emit
- The four golden signals: request rate, error rate, latency (as a histogram, not an average), and saturation (CPU, memory, queue depth, connection pool usage).
- Use a counter for request/error totals, a histogram for latency (so you can compute p50/p95/p99 later), and a gauge for in-flight/concurrency.
- Emit through OpenTelemetry's metrics API so the instrumentation is vendor-neutral; export to Prometheus (pull) or via OTLP to a collector (push), whichever your platform already runs.
Logs second: structured, correlated
- JSON logs with a fixed schema: timestamp, service, severity, message, and critically trace_id/span_id so a log line can be jumped to from a trace and vice versa.
- Log at request boundaries and on errors, not on every internal function call. High-frequency debug logs belong behind a sampling flag or a log level you can raise on demand, not in the default hot path.
Traces third: selective, not universal
- Instrument service entry/exit, outbound calls (DB, cache, downstream HTTP/gRPC), and anything async (queue publish/consume).
- Do not span every internal function, coarse spans keep both storage cost and cognitive load down.
Keeping cardinality and overhead in check
- Cardinality is the product of your label combinations. The rule that matters most: never put an unbounded value (user ID, order ID, raw URL with path params, IP address) into a metric label. Use the route template (
/orders/:id, not/orders/8231) and coarse buckets like status code class (2xx/4xx/5xx) or exact code where the cardinality is bounded. - Sample traces instead of capturing all of them: a low baseline rate (order of a few percent) for normal traffic, with tail-based or error-based sampling that keeps 100% of traces containing an error or a latency outlier. That way the traces you actually need for debugging are the ones least likely to get sampled away.
- Batch and export asynchronously. Exporters (OTel SDK, Prometheus client) should never block the request path; use a bounded queue with backpressure so a slow collector degrades telemetry, not the service.
- Put a collector (OpenTelemetry Collector or an agent) between the app and the backends. It absorbs batching, retries, and protocol translation so the application code stays simple and the backend can be swapped later without redeploying every service.
Worked example: why label choice matters for cardinality
Say a metric http_requests_total is labeled by route, status_code, and region. With 50 route templates, 5 status-code classes, and 3 regions, the number of distinct time series is:
That is a completely reasonable number for a metrics backend to hold. Now suppose someone adds user_id as a label on a service with 2 million active users, keeping the other dimensions:
That single label change turns a 750-series metric into a 1.5-billion-series metric. This is the concrete mechanism behind "cardinality overhead": it is not fuzzy, it is a multiplication, and any unbounded label multiplies the total by its full range. That is why the rule is "never an unbounded value as a label," not "be careful with labels."
Trade-offs & pitfalls
- Waiting for "the right instrumentation plan" before shipping any telemetry is worse than shipping imperfect metrics on day one and iterating. A service with zero observability during its riskiest early weeks is the actual failure mode this question is testing for.
- Over-instrumenting traces early (spanning every function) creates noisy, expensive traces that nobody reads, and by the time someone wants to prune it, dashboards and alerts already depend on the noise.
- Adding a label "just in case it's useful for debugging" is how cardinality incidents happen. If you need per-user detail, get it from logs or traces (where cardinality is expected and priced differently), not from a metric label.
- In a Kubernetes shop specifically, the same sequencing shows up as: define a ServiceMonitor or PodMonitor (Kubernetes custom resources that tell a running Prometheus which pods or services to scrape, and how) with a sane scrape interval (15 to 30 seconds is a common starting point, tighter intervals multiply storage cost across every pod), and scope the Prometheus service account's RBAC (role-based access control: Kubernetes' permission system for which service accounts can read or act on which resources) down to only what it needs to discover and scrape targets, before wiring in log shipping and trace collection as sidecars or DaemonSet agents (a DaemonSet runs exactly one copy of a pod on every node, which is why it's the natural fit for a per-node log or trace collector).
Write a log-formatting utility that safely serializes structured log entries to JSON, handles objects with circular references, and redacts any field whose name matches a configurable list of sensitive-field patterns before the entry is written out.
Sample Answer
Direct answer
Walk the object graph depth-first, track which objects are currently ancestors of the node you're visiting (not just "have I ever seen this object"), replace any object still on that ancestor chain with a circular-reference marker, and redact any key whose name matches a configured pattern before it's included in the output. The distinction between "ancestor" and "ever seen" matters: a naive "seen before" check falsely flags a shared, non-cyclic reference (the same sub-object reached twice, but not a real cycle) as circular.
Structured elaboration
Approach
- Recurse through the value. For primitives and null, return them (with light handling for values JSON can't represent, like BigInt).
- Maintain a Set of "ancestors": objects currently on the path from the root down to the node being visited. Add an object when entering it, remove it when leaving it (after finishing its children).
- If the object being visited is already in the ancestor set, it's a genuine cycle: emit a marker instead of recursing.
- For each key, check it against a list of redaction patterns (regexes or plain strings); if it matches, replace the value with a redaction marker without recursing into it (so a redacted object's own nested secrets don't leak either).
- Serialize the resulting sanitized structure with JSON.stringify.
function createSanitizer(redactPatterns = []) {
const redactors = redactPatterns.map((p) => (p instanceof RegExp ? p : new RegExp(p, 'i')));
const isRedactedKey = (key) => redactors.some((rx) => rx.test(key));
function sanitize(root) {
const ancestors = new Set(); // path-based, not "ever visited"
function walk(value) {
if (value === null || typeof value !== 'object') {
if (typeof value === 'bigint') return `[BigInt:${value.toString()}]`;
if (typeof value === 'function' || typeof value === 'symbol') return undefined;
return value;
}
if (ancestors.has(value)) return '[Circular]';
ancestors.add(value);
let result;
try {
if (Array.isArray(value)) {
result = value.map((v) => walk(v));
} else {
result = {};
for (const [k, v] of Object.entries(value)) {
if (isRedactedKey(k)) { result[k] = '[REDACTED]'; continue; }
const walked = walk(v);
if (walked !== undefined) result[k] = walked;
}
}
} finally {
ancestors.delete(value); // leaving this node: it's no longer an ancestor
}
return result;
}
return JSON.stringify(walk(root));
}
return sanitize;
}
Worked example
Running this exact code (Node.js, no external dependencies) against three cases:
const sanitize = createSanitizer(['token', 'password', 'ssn', '^creditCard$']);
// 1. A true circular reference
const a = { user: 'alice', token: 'abc123' };
a.self = a;
console.log(sanitize(a));
// 2. A shared but non-cyclic reference (same object reached via two paths)
const shared = { region: 'us-east-1' };
const b = { primary: shared, backup: shared, password: 'hunter2' };
console.log(sanitize(b));
// 3. Nested redaction inside an array
const c = { users: [{ name: 'bob', creditCard: '4111111111111111' }], ssn: '123-45-6789' };
console.log(sanitize(c));
Actual output:
{"user":"alice","token":"[REDACTED]","self":"[Circular]"}
{"primary":{"region":"us-east-1"},"backup":{"region":"us-east-1"},"password":"[REDACTED]"}
{"users":[{"name":"bob","creditCard":"[REDACTED]"}],"ssn":"[REDACTED]"}
Case 2 is the one that separates a correct implementation from a common near-miss: shared is referenced twice (as primary and backup), but it's never its own ancestor, so it correctly serializes in full both times instead of collapsing the second occurrence to [Circular]. An implementation that tracks "every object ever seen" (a WeakSet populated once and never cleared) would get this case wrong and silently drop real, non-redundant data from the log.
Key points
- Ancestor tracking (add on enter, remove on exit) distinguishes a real cycle from a shared DAG (directed acyclic graph: a node reachable by more than one path, but not by itself) reference.
- Redaction happens before recursion into the value, so a redacted key's children (which might themselves contain sensitive nested data) never get walked or leaked piecemeal.
- Function and symbol values are dropped rather than serialized (JSON has no representation for them); BigInt is stringified explicitly since JSON.stringify throws on raw BigInt.
Complexity
- Time: O(n) where n is the number of reachable properties/elements, since each is visited exactly once (a true cycle short-circuits back to the marker rather than re-walking).
- Space: O(n) for the sanitized clone, plus O(d) for the ancestor set where d is the maximum depth of the object graph (not n, since ancestors are removed on exit).
Edge cases
- A shared (non-cyclic) reference appearing multiple times: must serialize in full each time, not collapse (shown in the worked example above).
- Very deep, non-cyclic graphs can still overflow the call stack; a depth limit with a truncation marker is a reasonable production safeguard even though it isn't shown here.
- Keys matching a redaction pattern by name but holding non-sensitive values (e.g., a field literally named "password_policy" matching a "password" pattern) will still get redacted; patterns need to be specific enough to avoid over-redacting fields you actually want in logs.
- Array elements go through the same ancestor/redaction logic as object values, including nested circular arrays (an array containing itself).
Trade-offs & pitfalls
- Regex-based key matching is simple but can both over-redact (matching an unrelated field whose name happens to contain "token") and under-redact (a sensitive field named something the pattern list didn't anticipate); pairing it with a small allowlist review during code review catches drift over time.
- Doing this synchronously on every log call adds CPU work on the hot path proportional to payload size; for very high-throughput services, consider sampling deep objects or moving sanitization to an async logging pipeline stage instead of inline in the request path.
- Centralizing the redaction pattern list (rather than letting each service define its own) is what actually prevents a sensitive field from leaking through one service that forgot to add it to its local list.
- Redacting at the source (in this utility, before the log line is even constructed) is safer than redacting downstream at ingestion, since it means the unredacted value never leaves the process or crosses the network in the first place.
How do you make sure your monitoring and alerting system itself is trustworthy: that collectors are emitting correctly, that alert rules actually fire when they should, and that dashboards render without silently breaking? How would you build that verification into CI/CD rather than finding out during a real incident?
Sample Answer
Direct answer
Treat the monitoring stack itself as a product with its own test suite: unit tests for collectors and instrumentation, integration tests that exercise the real pipeline end to end, synthetic failure injection to prove alerts actually fire, and a pre-production gate that blocks a change from reaching real production until all of that passes. The goal is to find out a collector silently stopped emitting, or an alert rule got broken, in CI rather than during the next real incident.
Structured elaboration
Test layers and what each one catches
- Unit tests on collectors and exporters: mock the data source, feed malformed input, and assert the collector handles it (drops with a counted error, doesn't crash) and that metric names/labels/types match what dashboards and alerts expect.
- Integration tests: deploy the instrumented app plus the real telemetry pipeline (collector, processor, storage) to an ephemeral environment and verify metrics/traces/logs actually flow through and are queryable, not just that the collector emitted them locally.
- Synthetic failure injection: deliberately trigger the condition an alert is supposed to catch (spike a metric past its threshold, kill a dependency) and assert the alert transitions to firing within its expected window, with the right severity and context in the payload.
- Runbook-link and dry-run verification: confirm every alert's linked runbook resolves and, where the runbook is itself automatable, that its steps execute successfully in dry-run mode against a test target.
- Pre-production alert validation: gate merges on all of the above passing in a pre-prod environment before a change to collectors, alert rules, or dashboards reaches real production.
graph LR
A[Unit tests on PR] --> B[Integration tests on merge]
B --> C[Synthetic failure injection]
C --> D[Runbook dry run verification]
D --> E[Pre production alert validation]
E --> F[Gate to production]
Building this into CI/CD
Run unit tests on every PR (fast, cheap), integration and synthetic-injection tests on merge or as a nightly/pre-release stage (slower, needs real infra), and treat the whole thing as a required gate rather than an optional check that can be skipped under deadline pressure, the same way you'd treat application test coverage.
Worked example
Make one synthetic-injection test concrete and fully specified. In the pre-prod pipeline, inject a metric spike that should cross an alert's threshold at time t=0, then poll the alerting API every 5 seconds up to 120 seconds, asserting the alert's state transitions to firing by t=90s (the rule's configured evaluation window plus a small margin):
t=0s inject metric spike above threshold
t=5s..85s poll alert API every 5s, expect state = pending
t=90s assert alert state == firing
t=120s timeout: if still not firing, fail the CI job
This is a designed CI acceptance criterion, not a claim about real production timing: the 90-second bound comes directly from the alert rule's own configured evaluation window, so the test is asserting the rule behaves the way it was configured to, and a rule that doesn't fire by then genuinely has a bug (a misconfigured for duration, a broken query) worth catching before it ships.
Trade-offs and pitfalls
Pre-prod parity gaps are the real risk here: staging traffic patterns, cardinality, and scale rarely match production exactly, so a synthetic test passing in pre-prod doesn't guarantee the same alert will behave identically under real production load. This is the same underlying problem as metric cardinality (the number of distinct label/tag combinations a signal can take): a low-traffic staging environment produces far fewer distinct combinations than production, so a rule or test that looks fine against staging's narrow label space can behave differently, and cost far more to evaluate, once real production traffic multiplies the combinations it sees. Flaky synthetic tests are a second real cost: if the injection or polling is timing-sensitive and occasionally fails for reasons unrelated to the alert rule itself, teams start ignoring red CI runs, which defeats the whole point. Finally, this test suite is necessary but not sufficient: it validates that the pipeline and alerts behave correctly against known, injected conditions, but it can't replace periodic production canaries, low-risk synthetic checks run continuously against real production (such as a heartbeat probe that must be acknowledged on a fixed interval), which catch the failure modes nobody thought to write a synthetic test for, including a silent failure of the monitoring pipeline itself.
Unlock Full Question Bank
Get access to all 42 Monitoring, Logging, and Observability interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.