Debugging production incidents using observability data (logs, metrics, traces)
Scope: Incident triage workflow, root cause analysis, distributed tracing for debugging, correlation IDs, runbook creation, blameless postmortems
Lines: 440
Last Updated: 2025-10-26
Use this skill when:
Don't use for:
Context:
1. DETECT: Alert fires or user reports issue
↓
2. ASSESS: Check dashboards (RED metrics: Rate, Errors, Duration)
↓
3. INVESTIGATE: Logs → Metrics → Traces (in that order)
↓
4. ISOLATE: Identify failing component/service
↓
5. MITIGATE: Rollback, scale, or patch
↓
6. RESOLVE: Fix root cause
↓
7. LEARN: Postmortem, runbook updates
Rate: Requests per second
Errors: Error rate (percentage)
Duration: Latency (p50, p95, p99)
Utilization: % time resource is busy Saturation: Degree of queued work Errors: Error count
Logs → What happened?
Metrics → How much/many?
Traces → Where is the bottleneck?
# 1. CHECK RATE: Sudden drop or spike?
sum(rate(http_requests_total[5m])) by (service)
# 2. CHECK ERRORS: Error rate increase?
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
* 100 # Percentage
# 3. CHECK DURATION: Latency spike?
histogram_quantile(0.95,
sum by (le, service) (
rate(http_request_duration_seconds_bucket[5m])
)
)
# 4. IDENTIFY FAILING SERVICE
# Look for:
# - Service with highest error rate
# - Service with highest latency increase
# - Service with rate drop (if upstream)
Step 1: Start with Logs (What happened?)
# Find errors around incident time
kubectl logs -l app=order-service --since=30m | grep -i error
# Example output:
# 2025-10-26 10:15:32 ERROR [trace_id=abc123] Database connection timeout
# 2025-10-26 10:15:45 ERROR [trace_id=def456] Payment gateway unreachable
# 2025-10-26 10:16:12 ERROR [trace_id=ghi789] Order processing failed
Step 2: Check Metrics (How widespread?)
# How many errors?
sum(rate(logs_total{level="error", service="order-service"}[5m]))
# Which endpoint?
sum by (endpoint) (rate(http_requests_total{status="500", service="order-service"}[5m]))
# When did it start?
delta(http_requests_total{status="500", service="order-service"}[1h])
Step 3: Trace Deep Dive (Where is the bottleneck?)
# Use trace_id from logs (abc123)
# Open Jaeger/Tempo UI → Search trace_id=abc123
Trace View:
order-service [200ms]
├─ validate-order [10ms]
├─ check-inventory [50ms]
├─ process-payment [5000ms] ← SLOW!
│ ├─ call-payment-gateway [4950ms] ← BOTTLENECK!
│ └─ update-ledger [50ms]
└─ send-confirmation [20ms]
Diagnosis: Payment gateway timeout (4950ms vs expected 100ms)
import uuid
import logging
from flask import Flask, request, g
app = Flask(__name__)
@app.before_request
def set_correlation_id():
# Extract or generate correlation ID
correlation_id = request.headers.get('X-Correlation-ID') or str(uuid.uuid4())
g.correlation_id = correlation_id
@app.after_request
def add_correlation_header(response):
response.headers['X-Correlation-ID'] = g.correlation_id
return response
# Configure logger with correlation ID
class CorrelationFilter(logging.Filter):
def filter(self, record):
record.correlation_id = getattr(g, 'correlation_id', 'N/A')
return True
logging.basicConfig(
format='%(asctime)s [correlation_id=%(correlation_id)s] %(levelname)s %(message)s'
)
logger = logging.getLogger(__name__)
logger.addFilter(CorrelationFilter())
@app.route('/api/orders', methods=['POST'])
def create_order():
logger.info("Creating order")
# Pass correlation ID to downstream services
headers = {'X-Correlation-ID': g.correlation_id}
response = requests.post('http://inventory-service/check', headers=headers)
logger.info("Order created successfully")
return {"order_id": "12345"}
# Log output:
# 2025-10-26 10:30:15 [correlation_id=abc-123-def] INFO Creating order
# 2025-10-26 10:30:16 [correlation_id=abc-123-def] INFO Order created successfully
Scenario: User reports checkout failed at 10:15 AM
1. Find user's trace:
- Search by user_id in Jaeger: user_id=user_789 timestamp>2025-10-26T10:10:00
- Or search by correlation_id if logged
2. Examine trace timeline:
checkout-service [10:15:32 - 10:15:37] 5s
├─ cart-service [10:15:32 - 10:15:33] 1s
├─ payment [10:15:33 - 10:15:38] 5s ← TIMEOUT!
└─ inventory [not reached]
3. Drill into payment span:
- Status: ERROR
- Error message: "Connection refused: payment-gateway:8080"
- Attributes:
- payment.amount: 99.99
- payment.method: credit_card
- retry.count: 3
4. Cross-reference with logs:
grep "correlation_id=trace_xyz" logs/payment-service.log
→ "2025-10-26 10:15:33 ERROR Failed to connect to payment gateway after 3 retries"
5. Check metrics at 10:15:
- payment_gateway_connections{state="failed"} → spike at 10:15
- payment_gateway_health → DOWN at 10:14
6. Root cause: Payment gateway crash at 10:14, user checkout at 10:15 failed
import logging
from opentelemetry import trace
from sqlalchemy import event, create_engine
tracer = trace.get_tracer(__name__)
logger = logging.getLogger(__name__)
engine = create_engine('postgresql://localhost/mydb')
@event.listens_for(engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
# Start span for SQL query
span = tracer.start_span("db.query")
span.set_attribute("db.statement", statement)
span.set_attribute("db.system", "postgresql")
context._span = span
context._query_start_time = time.time()
@event.listens_for(engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
duration = time.time() - context._query_start_time
context._span.set_attribute("db.duration_ms", duration * 1000)
context._span.end()
# Log slow queries
if duration > 1.0:
logger.warning(f"Slow query ({duration:.2f}s): {statement[:100]}")
# Usage
with tracer.start_as_current_span("get_user_orders"):
user = session.query(User).filter_by(id=user_id).first() # Auto-traced
orders = session.query(Order).filter_by(user_id=user_id).all() # Auto-traced
# Trace shows:
# get_user_orders [150ms]
# ├─ db.query [10ms] "SELECT * FROM users WHERE id = ?"
# └─ db.query [140ms] "SELECT * FROM orders WHERE user_id = ?" ← SLOW!
Runbook Template:
# Incident: Payment Gateway Timeout (2025-10-26)
## Symptoms
- Users unable to complete checkout
- Error: "Payment processing failed, please try again"
- Alert: `payment_errors_rate > 5%`
## Detection
- Alert fired: `payment_gateway_health == 0`
- Dashboard: RED metrics show 50% error rate on `/api/checkout`
## Investigation
1. Check RED metrics:
```promql
sum(rate(http_requests_total{service="payment", status="500"}[5m]))
```
2. Find failing requests in logs:
```bash
kubectl logs -l app=payment-service --since=30m | grep ERROR
```
3. Trace sample failed request:
- Search Jaeger: service=payment status=error time>now-30m
- Identify span with error
## Diagnosis
- Payment gateway connection timeout (5s)
- Root cause: Payment gateway service crash
## Mitigation (Immediate)
1. Check payment gateway health:
```bash
kubectl get pods -l app=payment-gateway
```
2. Restart payment gateway:
```bash
kubectl rollout restart deployment/payment-gateway
```
3. Monitor recovery:
```promql
payment_gateway_health
```
## Resolution (Long-term)
- Implement circuit breaker pattern
- Add retry with exponential backoff
- Set up redundant payment gateway instances
- Add alerting for payment gateway health
## Prevention
- Deploy Istio circuit breaker config
- Add PodDisruptionBudget for payment gateway
- Implement graceful degradation (queue payments for retry)
## Related Incidents
- [2025-09-15] Payment gateway timeout (similar root cause)
- [2025-08-22] Database connection pool exhaustion
## Postmortem
- [Link to postmortem doc]
Postmortem Template:
# Postmortem: Payment Gateway Outage (2025-10-26)
## Incident Summary
- **Date**: 2025-10-26 10:14 - 10:45 UTC
- **Duration**: 31 minutes
- **Severity**: SEV-2 (Revenue impact)
- **Impact**: 15% of checkout attempts failed (~$50k revenue at risk)
## Timeline
- **10:14**: Payment gateway pod crash (OOM)
- **10:15**: First user reports checkout failure
- **10:16**: Alert fired: `payment_errors_rate > 5%`
- **10:18**: On-call engineer paged
- **10:22**: Investigation started
- **10:28**: Payment gateway restarted
- **10:30**: Health check passed
- **10:35**: Error rate returned to normal
- **10:45**: Incident marked resolved
## Root Cause
Payment gateway pod exceeded memory limit (2GB) due to connection leak in payment provider SDK v1.2.3. Under high load, SDK failed to close idle connections, leading to OOM crash.
## What Went Well
- Alert fired within 2 minutes of first error
- Correlation IDs enabled quick trace lookup
- Distributed tracing pinpointed exact failing span
- Runbook had clear mitigation steps
## What Went Wrong
- No memory usage alerting for payment gateway
- Single replica of payment gateway (no redundancy)
- Circuit breaker not configured (cascading failures)
- SDK version not pinned (auto-updated to buggy version)
## Action Items
1. **[P0]** Add memory usage alerts for all critical services (Owner: @alice, Due: 2025-10-28)
2. **[P0]** Scale payment gateway to 3 replicas with PDB (Owner: @bob, Due: 2025-10-27)
3. **[P1]** Implement circuit breaker with 10s timeout (Owner: @charlie, Due: 2025-11-02)
4. **[P1]** Pin SDK versions in requirements.txt (Owner: @alice, Due: 2025-10-27)
5. **[P2]** Add integration tests for payment flow (Owner: @bob, Due: 2025-11-10)
6. **[P2]** Update runbook with memory debugging steps (Owner: @charlie, Due: 2025-10-30)
## Lessons Learned
- **Observability gap**: Missing memory metrics for critical services
- **Architecture gap**: Single point of failure (payment gateway)
- **Process gap**: SDK updates without testing
## Follow-up
- Review all critical services for similar gaps
- Implement mandatory testing for dependency updates
# Check pod health
kubectl get pods -l app=service-name
# View recent logs
kubectl logs -l app=service-name --since=30m --tail=100
# Stream logs
kubectl logs -f -l app=service-name
# Get pod metrics
kubectl top pods -l app=service-name
# Describe pod (events)
kubectl describe pod service-name-abc123
# Execute into pod
kubectl exec -it service-name-abc123 -- /bin/bash
# Port forward for debugging
kubectl port-forward service-name-abc123 8080:8080
# Error rate (percentage)
sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
/
sum(rate(http_requests_total[5m])) by (service)
* 100
# Latency increase (current vs 1h ago)
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
/
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m] offset 1h))
# Request rate change
sum(rate(http_requests_total[5m])) by (service)
/
sum(rate(http_requests_total[5m] offset 1h)) by (service)
# Memory usage
container_memory_usage_bytes{pod=~"service-.*"} / 1024 / 1024 # MB
# CPU usage
rate(container_cpu_usage_seconds_total{pod=~"service-.*"}[5m]) * 100 # %
# By service and status
service=order-service status=error
# By tag
http.status_code=500
user_id=user_789
# By time range
service=payment minDuration=1s maxDuration=10s
# By operation
service=checkout operation=/api/checkout
# WRONG: Jump straight to traces
1. Alert fires → 2. Search traces for errors
# CORRECT: Follow workflow
1. Alert fires → 2. Check RED metrics → 3. Find pattern → 4. Sample traces
# WRONG: No request tracking across services
def call_service_a():
requests.post('http://service-a/api')
def call_service_b():
requests.post('http://service-b/api')
# CORRECT: Propagate correlation ID
def call_service(url):
headers = {'X-Correlation-ID': g.correlation_id}
requests.post(url, headers=headers)
# WRONG: Blame-focused
## Root Cause
Bob deployed buggy code without testing.
# CORRECT: System-focused
## Root Cause
Deployment pipeline lacked integration tests, allowing buggy SDK update to reach production.
## Action Items
- Add integration tests to CI/CD pipeline
- Implement mandatory code review for dependency updates
# WRONG: No ownership or deadlines
## Action Items
- Add alerts
- Fix bug
- Update runbook
# CORRECT: Clear ownership and deadlines
## Action Items
1. **[P0]** Add memory alerts (Owner: @alice, Due: 2025-10-28)
2. **[P1]** Fix connection leak (Owner: @bob, Due: 2025-11-02)
3. **[P2]** Update runbook (Owner: @charlie, Due: 2025-10-30)
Last Updated: 2025-10-26 Format Version: 1.0 (Atomic)