Observability - Complete Deep Dive
Prerequisites: Performance Metrics, Heartbeat & Health Checks Used in: the last five minutes of every senior interview β βhow would you know this system is healthy?β
Monitoring vs Observability
| Β | Monitoring | Observability |
|---|---|---|
| Answers | Known questions (βis CPU high?β) | Unknown questions (βwhy are Android users in Mumbai slow since 3pm?β) |
| Built from | Predefined dashboards and alerts | High-dimensional data you can slice arbitrarily |
| Failure mode | You only see what you thought to measure | Cost and cardinality |
Say this: βMonitoring tells me that something is wrong; observability lets me find out why without shipping new code. The practical test is whether I can answer a question I didnβt anticipate β if I have to add a metric and redeploy to debug, I have monitoring, not observability.β
The three pillars
| Β | Metrics | Logs | Traces |
|---|---|---|---|
| What | Numeric aggregates over time | Discrete timestamped events | The path of one request across services |
| Answers | βIs it broken? How much?β | βWhat exactly happened here?β | βWhere is the time going?β |
| Cost | Cheap (pre-aggregated) | Expensive at volume | Expensive (usually sampled) |
| Cardinality | Low β this is the hard constraint | High is fine | High is fine |
| Retention | Long (months-years, downsampled) | Short (days-weeks) | Short, sampled |
| Tools | Prometheus, CloudWatch, Datadog | ELK, Loki, CloudWatch Logs | Jaeger, Tempo, X-Ray, Honeycomb |
| Use it to | Alert and trend | Debug a specific failure | Find the slow hop in a distributed call |
The workflow that ties them together:
Metric alert fires: "checkout p99 > 1s"
β dashboard: which service? β payments
β trace: where in the request? β 800ms in the fraud-check call
β logs for that trace ID β "fraud model cache miss, falling back to sync scoring"
β root cause, in about 90 seconds
This is the answer to βhow would you debug a latency regression?β β metrics to detect, traces to localize, logs to explain. Naming that sequence is worth more than listing tools.
Metrics
The four golden signals (Google SRE)
| Signal | Question | Typical metric |
|---|---|---|
| Latency | How slow? | p50/p99 histogram, split successful vs failed |
| Traffic | How much? | rps, qps, concurrent connections |
| Errors | How often wrong? | 5xx rate, exception rate, plus silent failures |
| Saturation | How full? | CPU, memory, queue depth, connection pool utilization |
β οΈ Split latency by success and failure. A fast-failing 500 looks like a latency improvement if you bucket everything together β errors returning in 2ms will pull your p99 down while the system burns.
RED vs USE β two lenses, different subjects
| Β | RED | USE |
|---|---|---|
| Applies to | Services (request-driven) | Resources (CPU, disk, pool, queue) |
| Measures | Rate, Errors, Duration | Utilization, Saturation, Errors |
| Perspective | The userβs | The machineβs |
Say this: βRED for every service β rate, errors, duration β because thatβs what the user experiences. USE for every resource behind it, because saturation is the leading indicator: queue depth and pool utilization rise before latency does, which is what makes them useful for autoscaling and for paging early.β
Metric types
| Type | Meaning | Example | Gotcha |
|---|---|---|---|
| Counter | Monotonically increasing | requests_total |
Always graph the rate, never the raw value; and handle resets on restart |
| Gauge | Point-in-time value, up or down | queue_depth, memory_bytes |
Sampled β you miss spikes between scrapes |
| Histogram | Bucketed distribution | request_duration_seconds |
Buckets are fixed at definition; a bad bucket layout means you canβt compute a useful p99 |
| Summary | Client-computed quantiles | p99 per instance | Not aggregatable across instances β this is why histograms win |
β οΈ Prefer histograms over summaries. You cannot merge per-instance p99s into a fleet p99 (see Performance Metrics). Histograms merge; quantiles donβt.
Cardinality explosion β the #1 real-world observability incident
Every unique combination of label values is a separate time series.
http_requests_total{method, status, endpoint}
5 methods Γ 10 statuses Γ 50 endpoints = 2,500 series β fine
...now add user_id:
2,500 Γ 1,000,000 users = 2,500,000,000 series β your metrics
system is dead
Never put these in a metric label: user ID, request ID, trace ID, email, session ID, full URL path with IDs in it (/orders/12345), raw error message, timestamp.
Where high-cardinality data belongs: logs and traces. Theyβre indexed for it; metrics arenβt.
β metric label: endpoint="/orders/12345"
β
metric label: endpoint="/orders/:id" β normalize the template
β
trace attribute / log field: order_id=12345
Say this: βIβd keep metric labels low-cardinality β normalized route templates, status class, region β and push per-request identifiers into traces and logs. Cardinality explosion from putting a user ID in a label is the most common way teams take down their own monitoring, right at the moment they need it.β
Logs
Structured, always
β log.info("User " + userId + " failed payment of " + amt + " reason " + r);
β unqueryable; you're writing regexes at 3am
β
log.info("payment_failed",
Map.of("user_id", userId, "amount_cents", amt,
"reason", r, "trace_id", traceId));
β queryable: reason:insufficient_funds AND amount_cents:>10000
Levels, used consistently
| Level | Meaning | Alertable? |
|---|---|---|
| ERROR | Something failed that needs a human eventually | Sometimes (on rate, not per-event) |
| WARN | Unexpected but handled β retry succeeded, fallback used | No, but trend it |
| INFO | Notable business events β order placed, user registered | No |
| DEBUG | Developer detail | Off in prod, or sampled/dynamically enabled |
β οΈ If everything is ERROR, nothing is. Log-level discipline is what makes βerror rateβ a usable signal.
The costs people forget
- Volume dominates the bill. 10K rps Γ 2KB per log = 20MB/s = ~1.7TB/day. At that scale, DEBUG logging in production is a five-figure monthly line item.
- Logging is synchronous unless you make it async. A blocking write to a slow disk or a full log pipeline adds latency to every request β logging has caused real outages this way.
- PII in logs is a compliance problem. Redact at the emitter, not in the pipeline, because once itβs stored you have a deletion obligation.
Sampling strategy: log all errors, sample successes (1%), and support dynamic log levels so you can turn DEBUG on for one service or one user without a redeploy.
Distributed tracing
trace_id: abc123 (one user request, propagated through every hop)
[gateway ββββββββββββββββββββββββββββββββββββββββββββ 450ms]
[auth ββ 20ms]
[orders βββββββββββββββββββββββββββββββββββ 400ms]
[db query ββ 30ms]
[inventory ββββββ 90ms]
[payments ββββββββββββββββββββ 260ms] β there it is
[fraud-check ββββββββ 200ms] β actually, there
| Term | Meaning |
|---|---|
| Trace | One end-to-end request |
| Span | One unit of work within it (a service call, a DB query) |
| Parent span | The span that caused this one β builds the tree |
| Trace context | The headers propagated between services (W3C traceparent) |
| Baggage | Key-value data carried along the whole trace (tenant ID, experiment group) |
Propagation is the whole game. If any service drops the trace header, the trace breaks there and everything downstream looks like an unrelated trace. Instrument it once in a shared HTTP/gRPC client (or get it free from a service mesh) rather than per-endpoint β and remember queues: trace context must be carried in message headers too, or async work becomes invisible.
Sampling:
| Strategy | How | Trade-off |
|---|---|---|
| Head-based | Decide at the entry point (e.g. keep 1%) | Cheap, simple β but you probably didnβt sample the one slow request you care about |
| Tail-based | Buffer the full trace, decide after it finishes | Keeps all errors and slow traces; needs a collector holding spans in memory |
Say this: βTail-based sampling if the platform supports it β I want 100% of errors and slow traces and 1% of the boring ones. Head-based sampling at 1% is cheap but statistically guarantees you miss most of the rare failures youβre trying to debug.β
Alerting
Alert on symptoms, not causes
β "CPU > 80%" β often fine, and doesn't mean users hurt
β "a pod restarted" β that's the system self-healing
β "disk 70% full" β not urgent unless it's growing fast
β
"checkout error rate > 1% for 5 min" β users are failing
β
"p99 latency > 1s for 10 min" β users are waiting
β
"queue depth growing for 15 min" β users will fail soon
β
"error budget burn rate 14Γ normal" β we'll blow the SLO in 2 days
The test for a good alert: if it fires and nobody needs to do anything, it should not have fired. Every non-actionable page trains the on-call to ignore alerts β thatβs alert fatigue, and itβs how real incidents get missed.
Symptom vs cause, in practice
Page on symptoms (user-visible). Route causes to dashboards and tickets. One symptom alert plus a good dashboard beats twenty cause alerts, because the twenty all fire together during an incident and bury the signal.
Burn-rate alerting (the SRE approach)
Instead of βerror rate > 1%,β alert on how fast youβre consuming the error budget:
SLO: 99.9% over 30 days β error budget = 43 minutes
Burn rate 1Γ β you'll exactly use the budget in 30 days β fine
Burn rate 14Γ β budget gone in ~2 days β page now
Burn rate 6Γ β budget gone in ~5 days β ticket, not a page
Two windows (a fast one to catch spikes, a slow one to catch slow bleeds) gives you high precision and recall β far better than a fixed threshold that either pages on every blip or misses a slow degradation.
Things worth alerting on that people forget
- Absence of data β a metric going silent usually means the exporter died, and a silent system looks perfectly healthy on a dashboard
- Certificate expiry at 30 days
- Consumer lag growing (the earliest signal that async processing is falling behind)
- DLQ depth > 0 β messages are being permanently dropped from the happy path
- Replication lag exceeding your read-your-writes assumption
- Cron/batch job didnβt run β nothing failing means nothing alerting
What to instrument in an HLD answer
When asked βhow would you monitor this?β, walk the request path:
| Layer | Watch |
|---|---|
| Edge / CDN | Cache hit ratio, origin request rate, 4xx/5xx split, TLS errors |
| Load balancer | Per-target health, surge queue length, rejected connections, target response time |
| Service | RED per endpoint, saturation (thread pool, connection pool), GC pause time |
| Cache | Hit ratio (the metric), eviction rate, memory used vs max, hot-key skew |
| Database | p99 query time, slow query log, connection count vs max, replication lag, lock waits, transaction age |
| Queue | Consumer lag / queue depth, oldest message age, DLQ depth, processing rate vs arrival rate |
| Business | Orders/min, signups/min, payment success rate β the fastest detector of a subtle break |
β οΈ Business metrics catch what technical metrics miss. A deploy that makes the checkout button invisible produces a perfect technical dashboard β 200s everywhere, low latency β while revenue goes to zero. βOrders per minute vs the same time last weekβ catches it in minutes. Mentioning this is a strong senior signal.
The two saturation metrics most worth alerting on: queue depth and connection pool utilization. Both rise before latency does, which makes them leading rather than lagging indicators.
Interview application
βHow would you know this system is healthy?β βRED metrics per service and USE per resource, with the SLO defined on the user-facing indicator β checkout success rate and p99 latency at the edge. Iβd page on symptoms using multi-window burn-rate alerts against the error budget, not on CPU. For debugging, distributed tracing with W3C context propagated through HTTP and message headers, tail-sampled so I keep every error and slow trace, and structured logs carrying the trace ID so I can jump from a trace span straight to its logs.β
βA customer says the app is slow. Walk me through it.β βCheck whether itβs them or everyone: is the fleet p99 elevated, and does it slice to a region, client version, or tenant? If itβs global, find the slow hop in a trace β that gives me the service and often the specific call. Then the logs for that trace ID explain why. If the metrics look clean, I suspect something outside my measurement: DNS, mobile radio latency, or a client-side regression β which is why I want RUM at the client, not just server-side timing.β
βHow do you avoid alert fatigue?β βOnly page on symptoms that need immediate human action, use burn-rate windows so brief blips donβt page, route everything else to dashboards and tickets, and review every page in the postmortem asking βwas this actionable?β β deleting alerts is real work that teams under-invest in.β
Common Interview Questions
Q: βMetrics, logs, or traces β where do you start?β A: βMetrics to detect and quantify, traces to localize, logs to explain. Theyβre a workflow, not alternatives. If I could only have one for a distributed system Iβd take traces, because they contain enough structure to derive rough metrics, whereas metrics can never tell me where in a call graph the time went.β
Q: βWhatβs cardinality explosion?β A: βEvery unique label-value combination is a separate time series. Adding a user ID label to a metric multiplies your series count by your user count and kills the metrics backend β usually during an incident, when you need it most. High-cardinality identifiers belong in traces and logs; metric labels should be normalized route templates and low-cardinality dimensions.β
Q: βWhy not alert on CPU?β A: βHigh CPU often means good utilization, and low CPU can coexist with terrible latency from lock contention or a saturated connection pool. CPU is a cause, not a symptom. Iβd alert on user-visible symptoms and keep CPU on the dashboard for diagnosis.β
Q: βHow do you trace a request through async processing?β A: βPropagate trace context in the message headers, not just HTTP headers, and link the consumer span to the producer span. Without that, everything after the queue looks like an unrelated trace and the async half of your system is invisible.β
Q: βWhatβs the most important cache metric?β A: βHit ratio β it directly tells me whether the cache is earning its cost. Paired with eviction rate: a falling hit ratio with a high eviction rate means the cache is too small, whereas a falling hit ratio with low evictions means the access pattern changed or TTLs are too short.β
Q: βHow much do you sample?β A: βTail-based: 100% of errors and slow traces, ~1% of successes. Head-based sampling at a flat 1% is cheaper but means you almost never captured the rare slow request youβre being asked about.β
Q: βWhat would you alert on for a Kafka consumer?β A: βConsumer lag and its trend β absolute lag spikes during a deploy and thatβs fine, but lag growing steadily means arrival rate exceeds processing rate and youβll never catch up. Also oldest-message age, DLQ depth above zero, and the absence of processing metrics entirely, since a dead consumer emits nothing and looks calm.β
Decision Framework
| Question | Use |
|---|---|
| Is the system broken right now? | Metrics + symptom alerts |
| Which service is slow? | Distributed trace |
| Why did this specific request fail? | Logs, found via trace ID |
| Should this page someone? | Only if a human must act now |
| Where does a user ID belong? | Trace attribute or log field β never a metric label |
| How do I catch a silent functional break? | Business metrics vs same-time-last-week |
| Am I about to be overloaded? | Saturation: queue depth, pool utilization |
| Is async processing keeping up? | Consumer lag trend + oldest message age |
| How do I set the alert threshold? | Error-budget burn rate, multi-window |
| β Back to Fundamentals | β Unique ID Generation | Terminology Reference β |