Observability
Decision Framework: What to Instrument
Golden Signals (prefer for services): Latency, Traffic, Errors, Saturation
- RED (request-scoped): Rate, Errors, Duration
- USE (resource-scoped): Utilization, Saturation, Errors
Pick RED for microservices, USE for infrastructure. Don't mix.
Metric Design Opinions
- Always use histograms over summaries for latency -- histograms are aggregatable, summaries are not
- Bucket defaults
[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5] cover most HTTP services
- Exclude
/health and /metrics endpoints from SLI calculations
- Use recording rules for any query used in alerts or dashboards -- never put raw PromQL in alerts
- Label cardinality kills Prometheus: never use user IDs, request IDs, or unbounded values as labels
Service Tier Classification
| Tier |
Availability |
Latency P99 |
Examples |
| Critical |
99.95% |
100ms |
Payment, auth |
| Essential |
99.9% |
500ms |
Search, catalog |
| Standard |
99.5% |
1s |
Recommendations |
| Best Effort |
99.0% |
2s |
Batch, reporting |
Assign tiers before writing SLOs. Tier drives alert routing and error budget policy.
SLO Framework
Error Budget Policy (non-negotiable escalation)
| Budget Remaining |
Action |
| >50% |
Normal velocity |
| 10-50% |
Postpone risky changes |
| 1-10% |
Freeze non-critical changes |
| 0% |
Feature freeze, reliability only |
Release Decision Matrix
| Budget Status |
Low Risk |
Medium Risk |
High Risk |
| Healthy |
Approve |
Approve |
Review |
| Warning |
Review |
Defer |
Block |
| Critical |
Defer |
Block |
Block |
| Exhausted |
Block |
Block |
Block |
Burn Rate Alert Thresholds
| Alert |
Burn Rate |
Short Window |
Action |
| Fast burn |
14.4x |
1h + 5m |
Page on-call |
| Slow burn |
3x |
6h + 30m |
Create ticket |
Multi-window burn rate is the only correct SLO alerting pattern. Single-window alerts produce false positives or miss slow degradation.
Progressive SLO Rollout
Start at 99.0% for 1 month baseline, then tighten: 99.5% (2 months) -> 99.9% (3 months) -> 99.95% (ongoing). Never set SLO tighter than current measured reliability.
SLO Templates
API service: availability (99.9% over 30d) + latency (95% of requests < 500ms over 30d)
Data pipeline: freshness (99% batches within 30 min over 7d) + completeness (99.95% records processed over 7d)
Distributed Tracing Strategy
Sampling Decisions
- Dev/staging: 100% sampling
- Production low-traffic (<1k rps): 10-50% probabilistic
- Production high-traffic (>10k rps): 1% probabilistic or rate-limit to ~100 traces/sec
- Always use
ParentBased sampler so child spans follow parent's decision
- Force-sample all errors and high-latency requests regardless of probabilistic rate
Context Propagation
- Use W3C
traceparent header (not B3 or Jaeger-native) for new systems
- Always inject trace_id into structured logs for correlation
- Propagate context through async boundaries (queues, event buses) explicitly
Backend Choice
- Tempo (Grafana stack): prefer when already using Grafana; object-storage backed, cheap at scale
- Jaeger: prefer when you need standalone deployment or Elasticsearch integration
- Both support OTLP -- always send via OpenTelemetry Collector, never direct from app to backend
Alerting Opinions
- Alert on symptoms, not causes -- alert on error rate, not "pod restarted"
- Severity levels:
critical (pages), warning (tickets), info (dashboard only)
- Every alert must have a runbook link in annotations
for: duration: critical >= 2m, warning >= 5m, info >= 15m -- prevents flapping
- Route critical to PagerDuty, warning to Slack channel, info to dashboard only
Stack Preferences
| Concern |
Preferred Tool |
Rationale |
| Metrics |
Prometheus + Thanos/Mimir |
De facto standard, PromQL ecosystem |
| Visualization |
Grafana |
Dashboard-as-code, multi-datasource |
| Tracing |
Tempo or Jaeger via OTel |
OTLP-native, cost-effective |
| Logs |
Loki or OpenSearch |
Loki for Grafana stack, OpenSearch for complex queries |
| Collector |
OpenTelemetry Collector |
Vendor-neutral pipeline, single agent |
| Alerting |
Alertmanager |
Native Prometheus integration |
Gotchas
- Prometheus
rate() requires at least 2 data points in the window -- use [5m] minimum with 15s scrape interval
histogram_quantile is an estimate; accuracy degrades with poor bucket choices
- OTel Collector
batch processor default timeout is 200ms -- increase to 5-10s for production to reduce export overhead
- Grafana dashboards without variables become unmaintainable past 3 services
- Never scrape intervals faster than 10s in production -- it causes storage and CPU issues
- Alertmanager grouping: group by
alertname, namespace, service -- too broad silences everything, too narrow floods on-call
1---2name: observability-73description: Use when implementing metrics, tracing, SLOs, alerting, or dashboards. Covers Prometheus/Grafana/OTel stack design, SLI/SLO frameworks, error budgets, burn-rate alerting, and distributed tracing strategy.4---5
6# Observability
7
8## Decision Framework: What to Instrument
9
10**Golden Signals** (prefer for services): Latency, Traffic, Errors, Saturation
11- RED (request-scoped): Rate, Errors, Duration
12- USE (resource-scoped): Utilization, Saturation, Errors
13
14Pick RED for microservices, USE for infrastructure. Don't mix.
15
16## Metric Design Opinions
17
18- Always use histograms over summaries for latency -- histograms are aggregatable, summaries are not
19- Bucket defaults `[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 2, 5]` cover most HTTP services
20- Exclude `/health` and `/metrics` endpoints from SLI calculations
21- Use recording rules for any query used in alerts or dashboards -- never put raw PromQL in alerts
22- Label cardinality kills Prometheus: never use user IDs, request IDs, or unbounded values as labels
23
24## Service Tier Classification
25
26| Tier | Availability | Latency P99 | Examples |
27|------|-------------|-------------|----------|
28| Critical | 99.95% | 100ms | Payment, auth |
29| Essential | 99.9% | 500ms | Search, catalog |
30| Standard | 99.5% | 1s | Recommendations |
31| Best Effort | 99.0% | 2s | Batch, reporting |
32
33Assign tiers before writing SLOs. Tier drives alert routing and error budget policy.
34
35## SLO Framework
36
37### Error Budget Policy (non-negotiable escalation)
38
39| Budget Remaining | Action |
40|-----------------|--------|
41| >50% | Normal velocity |
42| 10-50% | Postpone risky changes |
43| 1-10% | Freeze non-critical changes |
44| 0% | Feature freeze, reliability only |
45
46### Release Decision Matrix
47
48| Budget Status | Low Risk | Medium Risk | High Risk |
49|--------------|----------|-------------|-----------|
50| Healthy | Approve | Approve | Review |
51| Warning | Review | Defer | Block |
52| Critical | Defer | Block | Block |
53| Exhausted | Block | Block | Block |
54
55### Burn Rate Alert Thresholds
56
57| Alert | Burn Rate | Short Window | Action |
58|-------|-----------|-------------|--------|
59| Fast burn | 14.4x | 1h + 5m | Page on-call |
60| Slow burn | 3x | 6h + 30m | Create ticket |
61
62Multi-window burn rate is the only correct SLO alerting pattern. Single-window alerts produce false positives or miss slow degradation.
63
64### Progressive SLO Rollout
65
66Start at 99.0% for 1 month baseline, then tighten: 99.5% (2 months) -> 99.9% (3 months) -> 99.95% (ongoing). Never set SLO tighter than current measured reliability.
67
68### SLO Templates
69
70**API service**: availability (99.9% over 30d) + latency (95% of requests < 500ms over 30d)
71**Data pipeline**: freshness (99% batches within 30 min over 7d) + completeness (99.95% records processed over 7d)
72
73## Distributed Tracing Strategy
74
75### Sampling Decisions
76- **Dev/staging**: 100% sampling
77- **Production low-traffic** (<1k rps): 10-50% probabilistic
78- **Production high-traffic** (>10k rps): 1% probabilistic or rate-limit to ~100 traces/sec
79- Always use `ParentBased` sampler so child spans follow parent's decision
80- Force-sample all errors and high-latency requests regardless of probabilistic rate
81
82### Context Propagation
83- Use W3C `traceparent` header (not B3 or Jaeger-native) for new systems
84- Always inject trace_id into structured logs for correlation
85- Propagate context through async boundaries (queues, event buses) explicitly
86
87### Backend Choice
88- **Tempo** (Grafana stack): prefer when already using Grafana; object-storage backed, cheap at scale
89- **Jaeger**: prefer when you need standalone deployment or Elasticsearch integration
90- Both support OTLP -- always send via OpenTelemetry Collector, never direct from app to backend
91
92## Alerting Opinions
93
94- **Alert on symptoms, not causes** -- alert on error rate, not "pod restarted"
95- Severity levels: `critical` (pages), `warning` (tickets), `info` (dashboard only)
96- Every alert must have a runbook link in annotations
97- `for:` duration: critical >= 2m, warning >= 5m, info >= 15m -- prevents flapping
98- Route critical to PagerDuty, warning to Slack channel, info to dashboard only
99
100## Stack Preferences
101
102| Concern | Preferred Tool | Rationale |
103|---------|---------------|-----------|
104| Metrics | Prometheus + Thanos/Mimir | De facto standard, PromQL ecosystem |
105| Visualization | Grafana | Dashboard-as-code, multi-datasource |
106| Tracing | Tempo or Jaeger via OTel | OTLP-native, cost-effective |
107| Logs | Loki or OpenSearch | Loki for Grafana stack, OpenSearch for complex queries |
108| Collector | OpenTelemetry Collector | Vendor-neutral pipeline, single agent |
109| Alerting | Alertmanager | Native Prometheus integration |
110
111## Gotchas
112
113- Prometheus `rate()` requires at least 2 data points in the window -- use `[5m]` minimum with 15s scrape interval
114- `histogram_quantile` is an estimate; accuracy degrades with poor bucket choices
115- OTel Collector `batch` processor default timeout is 200ms -- increase to 5-10s for production to reduce export overhead
116- Grafana dashboards without variables become unmaintainable past 3 services
117- Never scrape intervals faster than 10s in production -- it causes storage and CPU issues
118- Alertmanager grouping: group by `alertname, namespace, service` -- too broad silences everything, too narrow floods on-call