
Qa Observability
- 145 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
Helps with ai & agent building tasks.
About
qa-observability is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- qa-observability
- AI & Agent Building
- AI-coding skill
Qa Observability by the numbers
- 145 all-time installs (skills.sh)
- +2 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,405 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill qa-observabilityAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 145 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
What it does
Helps with ai & agent building tasks.
Files
QA Observability and Performance Engineering
Use telemetry (logs, metrics, traces, profiles) as a QA signal and a debugging substrate.
Core references (see data/sources.json): OpenTelemetry, W3C Trace Context, and SLO practices (Google SRE).
Quick Start (Default)
If key context is missing, ask for: critical user journeys, service/dependency inventory, environments (local/staging/prod), current telemetry stack, and current SLO/SLA commitments (if any).
1. Establish the minimum bar: correlation IDs + structured logs + traces + golden metrics (latency, traffic, errors, saturation). 2. Verify propagation: confirm traceparent (and your request ID) flow across boundaries end-to-end. 3. Make failures diagnosable: every test failure captures a trace link (or trace ID) plus the correlated logs. 4. Define SLIs/SLOs and error budget policy; wire burn-rate alerts (prefer multi-window burn rates). 5. Produce artifacts: a readiness checklist plus an SLO definition and alert rules (use assets/checklists/template-observability-readiness-checklist.md and assets/monitoring/slo/*).
Default QA stance
- Treat telemetry as part of acceptance criteria (especially for integration/E2E tests).
- Require correlation: request_id + trace_id (traceparent) across boundaries.
- Prefer SLO-based release gating and burn-rate alerting over raw infra thresholds.
- Budget overhead: sampling, cardinality, retention, and cost are quality constraints.
- Redact PII/secrets by default (logs and attributes).
Core workflows
1. Establish the minimum bar (logs + metrics + traces + correlation). 2. Instrument with OpenTelemetry (auto-instrument first, then add manual spans for key paths). 3. Verify context propagation across service boundaries (traceparent in/out). 4. Define SLIs/SLOs and error budget policy; wire burn-rate alerts. 5. Make failures diagnosable: capture a trace link + key logs on every test failure. 6. Profile and load test only after telemetry is reliable; validate against baselines.
Quick reference
| Task | Recommended default | Notes |
|---|---|---|
| Tracing | OpenTelemetry + Jaeger/Tempo | Prefer OTLP exporters via Collector when possible |
| Metrics | Prometheus + Grafana | Use histograms for latency; watch cardinality |
| Logging | Structured JSON + correlation IDs | Never log secrets/PII; redact aggressively |
| Reliability gates | SLOs + error budgets + burn-rate alerts | Gate releases on sustained burn/regressions |
| Performance | Profiling + load tests + budgets | Add continuous profiling for intermittent issues |
| Zero-code visibility | eBPF (OpenTelemetry zero-code) + continuous profiling (Parca/Pyroscope) | Use when code changes are not feasible |
Navigation
Open these guides when needed:
| If the user needs... | Read | Also use |
|---|---|---|
| A minimal, production-ready baseline | references/core-observability-patterns.md | assets/checklists/template-observability-readiness-checklist.md |
| Node/Python instrumentation setup | references/opentelemetry-best-practices.md | assets/opentelemetry/nodejs/opentelemetry-nodejs-setup.md, assets/opentelemetry/python/opentelemetry-python-setup.md |
| Working trace propagation across services | references/distributed-tracing-patterns.md | assets/checklists/template-observability-readiness-checklist.md |
| SLOs, burn-rate alerts, and release gates | references/slo-design-guide.md | assets/monitoring/slo/slo-definition.yaml, assets/monitoring/slo/prometheus-alert-rules.yaml |
| Profiling/load testing with evidence | references/performance-profiling-guide.md | assets/load-testing/load-testing-k6.js, assets/load-testing/template-load-test-artillery.yaml |
| A maturity model and roadmap | references/observability-maturity-model.md | assets/checklists/template-observability-readiness-checklist.md |
| What to avoid and how to fix it | references/anti-patterns-best-practices.md | assets/checklists/template-observability-readiness-checklist.md |
| Alert design and fatigue reduction | references/alerting-strategies.md | assets/monitoring/slo/prometheus-alert-rules.yaml |
| Dashboard hierarchy and layout | references/dashboard-design-patterns.md | assets/monitoring/grafana/template-grafana-dashboard-observability.json |
| Structured logging and cost control | references/log-aggregation-patterns.md | assets/observability/template-logging-setup.md |
Implementation guides (deep dives):
references/core-observability-patterns.mdreferences/opentelemetry-best-practices.mdreferences/distributed-tracing-patterns.mdreferences/slo-design-guide.mdreferences/performance-profiling-guide.mdreferences/observability-maturity-model.mdreferences/anti-patterns-best-practices.mdreferences/alerting-strategies.mdreferences/dashboard-design-patterns.mdreferences/log-aggregation-patterns.md
Templates (copy/paste):
assets/checklists/template-observability-readiness-checklist.mdassets/opentelemetry/nodejs/opentelemetry-nodejs-setup.mdassets/opentelemetry/python/opentelemetry-python-setup.mdassets/monitoring/slo/slo-definition.yamlassets/monitoring/slo/prometheus-alert-rules.yamlassets/monitoring/grafana/grafana-dashboard-slo.jsonassets/monitoring/grafana/template-grafana-dashboard-observability.jsonassets/load-testing/load-testing-k6.jsassets/load-testing/template-load-test-artillery.yamlassets/performance/frontend/template-lighthouse-ci.jsonassets/performance/backend/template-nodejs-profiling-config.js
Curated sources:
data/sources.json
Scope boundaries (handoffs)
- Pure infrastructure monitoring (Kubernetes, Docker, CI/CD):
../ops-devops-platform/SKILL.md - Database query optimization (SQL tuning, indexing):
../data-sql-optimization/SKILL.md - Application-level debugging (stack traces, breakpoints):
../qa-debugging/SKILL.md - Test strategy design (coverage, test pyramids):
../qa-testing-strategy/SKILL.md - Resilience patterns (retries, circuit breakers):
../qa-resilience/SKILL.md - Architecture decisions (microservices, event-driven):
../software-architecture-design/SKILL.md
Tool selection notes (2026)
- Default to OpenTelemetry + OTLP + Collector where possible.
- Prefer burn-rate alerting against SLOs over alerting on raw infra metrics.
- Treat sampling, cardinality, and retention as part of quality (not an afterthought).
- When asked to pick vendors/tools, start from
data/sources.jsonand validate time-sensitive claims with current docs/releases if the environment allows it.
Fact-Checking
- Use web search/web fetch to verify current external facts, versions, pricing, deadlines, regulations, or platform behavior before final answers.
- Prefer primary sources; report source links and dates for volatile information.
- If web access is unavailable, state the limitation and mark guidance as unverified.
Observability Readiness Checklist (QA)
Use this checklist before relying on telemetry for test or release decisions.
Core
Logs
- [ ] Structured logs (JSON) with consistent fields
- [ ] Correlation IDs present: request ID and trace ID (W3C Trace Context: https://www.w3.org/TR/trace-context/)
- [ ] Sensitive data redacted (no secrets/PII)
- [ ] Log levels enforced (debug not enabled by default in prod)
- [ ] Logs are searchable in a central store (with retention policy)
Metrics
- [ ] SLIs defined: availability, error rate, latency (p95/p99), saturation
- [ ] Dashboards exist for the above SLIs and have owners
- [ ] Baselines exist (last 7/30 days) so regressions are visible
- [ ] Alerting is tied to symptoms (SLO burn), not raw infra noise
Traces
- [ ] Trace propagation works across service boundaries (OpenTelemetry: https://opentelemetry.io/docs/)
- [ ] Critical spans exist for key operations (auth, checkout, search, etc.)
- [ ] Spans include useful attributes (route, status code, tenant, dependency)
- [ ] Sampling policy documented (example: 100% errors, sampled successes)
SLOs and Runbooks
- [ ] Service SLOs and error budget policy defined (Google SRE SLOs: https://sre.google/sre-book/service-level-objectives/)
- [ ] Alerts have runbooks with clear steps, owners, and rollback criteria
- [ ] On-call escalation paths and communication templates exist (if applicable)
QA and CI Integration
- [ ] Tests emit correlation IDs on failure (request/trace IDs in logs)
- [ ] Test runs are tagged (env, build SHA, suite, shard/worker)
- [ ] Failure artifacts are stored and linked (logs/traces/screenshots)
Cost and Privacy Guardrails
- [ ] Telemetry retention and sampling tuned for cost
- [ ] Access controls in place for logs/traces (least privilege)
- [ ] PII review completed for log/trace fields
Optional: AI / Automation
Do:
- Use AI to cluster alert storms and summarize incident timelines; verify via trace/log evidence.
- Use AI to propose missing telemetry fields and dashboards; review with owners.
Avoid:
- Auto-remediation without guardrails and human review for high-severity actions.
- "Black box" anomaly detection without explainability and rollback plans.
// k6 Load Testing Template
//
// Use this template for performance testing and capacity planning.
// Run: k6 run load-testing-k6.js
import http from 'k6/http';
import { check, sleep, group } from 'k6';
import { Rate, Trend, Counter, Gauge } from 'k6/metrics';
import { randomIntBetween, randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
// ====================
// Configuration
// ====================
// Test configuration
export const options = {
// Scenario 1: Load Test (ramp up to target load)
scenarios: {
load_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Stay at 50 users
{ duration: '2m', target: 100 }, // Ramp up to 100 users
{ duration: '5m', target: 100 }, // Stay at 100 users
{ duration: '2m', target: 200 }, // Ramp up to 200 users
{ duration: '5m', target: 200 }, // Stay at 200 users
{ duration: '2m', target: 0 }, // Ramp down
],
gracefulRampDown: '30s',
},
// Scenario 2: Spike Test (sudden traffic spike)
// spike_test: {
// executor: 'ramping-vus',
// startTime: '25m',
// stages: [
// { duration: '10s', target: 500 }, // Spike to 500 users
// { duration: '1m', target: 500 }, // Hold spike
// { duration: '10s', target: 0 }, // Drop to 0
// ],
// },
// Scenario 3: Soak Test (sustained load)
// soak_test: {
// executor: 'constant-vus',
// vus: 100,
// duration: '1h',
// },
},
// Performance thresholds (test fails if violated)
thresholds: {
// HTTP request duration
'http_req_duration': [
'p(95)<500', // 95% of requests < 500ms
'p(99)<1000', // 99% of requests < 1s
],
// HTTP request failed rate
'http_req_failed': [
'rate<0.01', // Error rate < 1%
],
// Custom metrics
'order_duration': ['p(95)<1000'],
'errors': ['rate<0.05'],
},
// HTTP configuration
httpDebug: 'full', // Change to 'full' for debugging
// Discard response bodies to save memory
discardResponseBodies: false, // Set to true for high-load tests
};
// ====================
// Custom Metrics
// ====================
const orderDuration = new Trend('order_duration');
const checkoutDuration = new Trend('checkout_duration');
const errorRate = new Rate('errors');
const orderCount = new Counter('orders_created');
const activeOrders = new Gauge('active_orders');
// ====================
// Configuration
// ====================
const BASE_URL = __ENV.BASE_URL || 'https://api.example.com';
const AUTH_TOKEN = __ENV.AUTH_TOKEN || 'your-test-token';
// Test data
const PRODUCT_IDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const USER_IDS = Array.from({ length: 100 }, (_, i) => i + 1);
// ====================
// Setup (runs once)
// ====================
export function setup() {
console.log('Starting load test');
console.log(`Base URL: ${BASE_URL}`);
// Health check
const res = http.get(`${BASE_URL}/health`);
check(res, {
'health check passed': (r) => r.status === 200,
});
return {
startTime: Date.now(),
};
}
// ====================
// Main Test Scenario
// ====================
export default function (data) {
const userId = randomItem(USER_IDS);
// Request headers
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_TOKEN}`,
'X-User-ID': userId.toString(),
};
// Scenario: Create Order Flow
group('Create Order Flow', function () {
// Step 1: Get products
group('Get Products', function () {
const res = http.get(`${BASE_URL}/api/products`, { headers });
check(res, {
'products: status 200': (r) => r.status === 200,
'products: has data': (r) => JSON.parse(r.body).data.length > 0,
}) || errorRate.add(1);
});
// Step 2: Create order
group('Create Order', function () {
const orderStart = Date.now();
const payload = JSON.stringify({
user_id: userId,
items: [
{
product_id: randomItem(PRODUCT_IDS),
quantity: randomIntBetween(1, 5),
},
],
});
const res = http.post(`${BASE_URL}/api/orders`, payload, { headers });
const duration = Date.now() - orderStart;
orderDuration.add(duration);
const success = check(res, {
'order: status 201': (r) => r.status === 201,
'order: has order_id': (r) => JSON.parse(r.body).order_id !== undefined,
'order: response time < 1s': (r) => r.timings.duration < 1000,
});
if (success) {
orderCount.add(1);
const order = JSON.parse(res.body);
activeOrders.add(1);
// Step 3: Process payment
group('Process Payment', function () {
const paymentPayload = JSON.stringify({
order_id: order.order_id,
payment_method: 'card',
amount: order.total,
});
const paymentRes = http.post(`${BASE_URL}/api/payments`, paymentPayload, { headers });
check(paymentRes, {
'payment: status 200': (r) => r.status === 200,
'payment: success': (r) => JSON.parse(r.body).status === 'success',
}) || errorRate.add(1);
sleep(0.5); // User waits for payment confirmation
});
} else {
errorRate.add(1);
}
});
});
// Scenario: Browse Products (80% of users)
if (Math.random() < 0.8) {
group('Browse Products', function () {
const res = http.get(`${BASE_URL}/api/products?page=1&limit=20`, { headers });
check(res, {
'browse: status 200': (r) => r.status === 200,
'browse: response time < 500ms': (r) => r.timings.duration < 500,
}) || errorRate.add(1);
});
}
// Scenario: View Order History (20% of users)
if (Math.random() < 0.2) {
group('View Order History', function () {
const res = http.get(`${BASE_URL}/api/orders?user_id=${userId}`, { headers });
check(res, {
'history: status 200': (r) => r.status === 200,
}) || errorRate.add(1);
});
}
// Think time (simulate user reading, clicking)
sleep(randomIntBetween(1, 3));
}
// ====================
// Teardown (runs once)
// ====================
export function teardown(data) {
const duration = (Date.now() - data.startTime) / 1000 / 60;
console.log(`Load test completed in ${duration.toFixed(2)} minutes`);
}
// ====================
// Advanced: Custom Summary
// ====================
export function handleSummary(data) {
// Print summary to stdout
console.log('=======================================');
console.log('Load Test Summary');
console.log('=======================================');
console.log(`Total Requests: ${data.metrics.http_reqs.values.count}`);
console.log(`Request Rate: ${data.metrics.http_reqs.values.rate.toFixed(2)} req/s`);
console.log(`Failed Requests: ${(data.metrics.http_req_failed.values.rate * 100).toFixed(2)}%`);
console.log(`P95 Latency: ${data.metrics.http_req_duration.values['p(95)'].toFixed(2)}ms`);
console.log(`P99 Latency: ${data.metrics.http_req_duration.values['p(99)'].toFixed(2)}ms`);
console.log('=======================================');
// Return JSON summary for CI/CD
return {
'summary.json': JSON.stringify(data, null, 2),
'stdout': textSummary(data, { indent: ' ', enableColors: true }),
};
}
// Helper for text summary
function textSummary(data, options) {
return 'Load test completed';
}
// ====================
// Usage Examples
// ====================
/*
1. Basic load test:
k6 run load-testing-k6.js
2. Custom target:
k6 run --vus 100 --duration 10m load-testing-k6.js
3. Custom base URL:
k6 run -e BASE_URL=https://staging.example.com load-testing-k6.js
4. Cloud execution:
k6 cloud load-testing-k6.js
5. CI/CD integration:
k6 run --out json=results.json load-testing-k6.js
6. Thresholds only (no summary):
k6 run --quiet load-testing-k6.js
7. Different scenarios:
k6 run --scenario spike_test load-testing-k6.js
*/
# Artillery Load Testing Template
#
# Alternative to k6 - YAML-based load testing
# Install: npm install -g artillery
# Run: artillery run load-test.yaml
config:
target: "https://api.example.com"
phases:
# Warm-up phase
- duration: 60
arrivalRate: 5
name: "Warm up"
# Ramp up to target load
- duration: 120
arrivalRate: 5
rampTo: 50
name: "Ramp up to 50 users/sec"
# Sustained load
- duration: 300
arrivalRate: 50
name: "Sustained load (50 users/sec)"
# Peak load
- duration: 120
arrivalRate: 50
rampTo: 100
name: "Ramp up to 100 users/sec"
# Peak sustained
- duration: 300
arrivalRate: 100
name: "Peak load (100 users/sec)"
# Ramp down
- duration: 60
arrivalRate: 100
rampTo: 0
name: "Ramp down"
# HTTP configuration
http:
timeout: 30
pool: 50 # Connection pool size
# Performance thresholds
ensure:
p95: 500 # P95 latency < 500ms
p99: 1000 # P99 latency < 1s
maxErrorRate: 1 # Error rate < 1%
# Variables
variables:
user_ids:
- "user-1"
- "user-2"
- "user-3"
- "user-4"
- "user-5"
product_ids:
- 101
- 102
- 103
- 104
- 105
# Plugins
plugins:
metrics-by-endpoint:
stripQueryString: true
expect: {}
scenarios:
# Scenario 1: Browse products (60% of users)
- name: "Browse Products"
weight: 60
flow:
- get:
url: "/api/products"
headers:
Authorization: "Bearer {{ $processEnvironment.AUTH_TOKEN }}"
X-User-ID: "{{ user_ids }}"
capture:
- json: "$.data[0].id"
as: "product_id"
expect:
- statusCode: 200
- contentType: json
- hasProperty: data
- think: 2 # User reads for 2 seconds
- get:
url: "/api/products/{{ product_id }}"
headers:
Authorization: "Bearer {{ $processEnvironment.AUTH_TOKEN }}"
expect:
- statusCode: 200
- think: 3
# Scenario 2: Create order (30% of users)
- name: "Create Order"
weight: 30
flow:
- post:
url: "/api/orders"
headers:
Authorization: "Bearer {{ $processEnvironment.AUTH_TOKEN }}"
Content-Type: "application/json"
X-User-ID: "{{ user_ids }}"
json:
items:
- product_id: "{{ product_ids }}"
quantity: "{{ $randomNumber(1, 5) }}"
capture:
- json: "$.order_id"
as: "order_id"
expect:
- statusCode: 201
- hasProperty: order_id
- think: 1
- get:
url: "/api/orders/{{ order_id }}"
headers:
Authorization: "Bearer {{ $processEnvironment.AUTH_TOKEN }}"
expect:
- statusCode: 200
# Scenario 3: View order history (10% of users)
- name: "View Order History"
weight: 10
flow:
- get:
url: "/api/orders?user_id={{ user_ids }}"
headers:
Authorization: "Bearer {{ $processEnvironment.AUTH_TOKEN }}"
expect:
- statusCode: 200
- contentType: json
# Custom metrics
# after:
# flow:
# - log: "Test completed"
# Reporting
# artillery run load-test.yaml --output report.json
# artillery report report.json --output report.html
{
"dashboard": {
"title": "Service SLO Dashboard",
"tags": ["slo", "observability", "production"],
"timezone": "browser",
"refresh": "1m",
"time": {
"from": "now-30d",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Availability SLI (30d Rolling)",
"type": "gauge",
"gridPos": { "x": 0, "y": 0, "w": 6, "h": 6 },
"targets": [
{
"expr": "sum(rate(http_requests_total{service=\"$service\",status!~\"5..\"}[30d])) / sum(rate(http_requests_total{service=\"$service\"}[30d]))",
"legendFormat": "Availability"
}
],
"options": {
"orientation": "auto",
"showThresholdLabels": true,
"showThresholdMarkers": true
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0.99,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0.99, "color": "red" },
{ "value": 0.995, "color": "yellow" },
{ "value": 0.999, "color": "green" }
]
}
}
}
},
{
"id": 2,
"title": "Error Budget Remaining",
"type": "gauge",
"gridPos": { "x": 6, "y": 0, "w": 6, "h": 6 },
"targets": [
{
"expr": "((0.999 - (1 - (sum(rate(http_requests_total{service=\"$service\",status!~\"5..\"}[30d])) / sum(rate(http_requests_total{service=\"$service\"}[30d]))))) / 0.001) * 100",
"legendFormat": "Budget Remaining"
}
],
"options": {
"orientation": "auto",
"showThresholdLabels": true,
"showThresholdMarkers": true
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "red" },
{ "value": 10, "color": "orange" },
{ "value": 25, "color": "yellow" },
{ "value": 50, "color": "green" }
]
}
}
}
},
{
"id": 3,
"title": "P99 Latency (Target: 500ms)",
"type": "gauge",
"gridPos": { "x": 12, "y": 0, "w": 6, "h": 6 },
"targets": [
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P99 Latency"
}
],
"options": {
"orientation": "auto",
"showThresholdLabels": true,
"showThresholdMarkers": true
},
"fieldConfig": {
"defaults": {
"unit": "s",
"min": 0,
"max": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "green" },
{ "value": 0.5, "color": "yellow" },
{ "value": 1.0, "color": "red" }
]
}
}
}
},
{
"id": 4,
"title": "Request Rate (req/s)",
"type": "stat",
"gridPos": { "x": 18, "y": 0, "w": 6, "h": 6 },
"targets": [
{
"expr": "sum(rate(http_requests_total{service=\"$service\"}[5m]))",
"legendFormat": "Requests/s"
}
],
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto"
},
"fieldConfig": {
"defaults": {
"unit": "reqps",
"decimals": 2
}
}
},
{
"id": 5,
"title": "SLI History (30 days)",
"type": "graph",
"gridPos": { "x": 0, "y": 6, "w": 12, "h": 8 },
"targets": [
{
"expr": "0.999",
"legendFormat": "Target (99.9%)"
},
{
"expr": "sum(rate(http_requests_total{service=\"$service\",status!~\"5..\"}[1h])) / sum(rate(http_requests_total{service=\"$service\"}[1h]))",
"legendFormat": "Actual SLI"
}
],
"yaxes": [
{
"format": "percentunit",
"min": 0.99,
"max": 1
}
],
"seriesOverrides": [
{
"alias": "Target (99.9%)",
"color": "#73BF69",
"dashes": true,
"fill": 0,
"linewidth": 2
},
{
"alias": "Actual SLI",
"color": "#5794F2",
"fill": 1,
"fillGradient": 5
}
]
},
{
"id": 6,
"title": "Error Budget Burn Rate (1h window)",
"type": "graph",
"gridPos": { "x": 12, "y": 6, "w": 12, "h": 8 },
"targets": [
{
"expr": "(1 - (sum(rate(http_requests_total{service=\"$service\",status!~\"5..\"}[1h])) / sum(rate(http_requests_total{service=\"$service\"}[1h])))) / (1 - 0.999)",
"legendFormat": "Burn Rate"
},
{
"expr": "14.4",
"legendFormat": "Critical Threshold (14.4x)"
},
{
"expr": "6",
"legendFormat": "Warning Threshold (6x)"
}
],
"yaxes": [
{
"format": "short",
"min": 0,
"max": 20
}
],
"seriesOverrides": [
{
"alias": "Critical Threshold (14.4x)",
"color": "#F2495C",
"dashes": true,
"fill": 0
},
{
"alias": "Warning Threshold (6x)",
"color": "#FF9830",
"dashes": true,
"fill": 0
}
]
},
{
"id": 7,
"title": "Latency Distribution (P50, P95, P99)",
"type": "graph",
"gridPos": { "x": 0, "y": 14, "w": 12, "h": 8 },
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P50"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P95"
},
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P99"
},
{
"expr": "0.5",
"legendFormat": "SLO Target (500ms)"
}
],
"yaxes": [
{
"format": "s",
"min": 0
}
],
"seriesOverrides": [
{
"alias": "SLO Target (500ms)",
"color": "#73BF69",
"dashes": true,
"fill": 0
}
]
},
{
"id": 8,
"title": "Error Rate by Status Code",
"type": "graph",
"gridPos": { "x": 12, "y": 14, "w": 12, "h": 8 },
"targets": [
{
"expr": "sum by (status) (rate(http_requests_total{service=\"$service\",status=~\"5..\"}[5m]))",
"legendFormat": "{{status}}"
}
],
"yaxes": [
{
"format": "reqps",
"min": 0
}
],
"stack": true
},
{
"id": 9,
"title": "Active Alerts",
"type": "table",
"gridPos": { "x": 0, "y": 22, "w": 24, "h": 6 },
"targets": [
{
"expr": "ALERTS{service=\"$service\",alertstate=\"firing\"}",
"format": "table",
"instant": true
}
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"Value": true,
"__name__": true
},
"indexByName": {
"alertname": 0,
"severity": 1,
"alertstate": 2,
"summary": 3
}
}
}
]
}
],
"templating": {
"list": [
{
"name": "service",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(http_requests_total, service)",
"multi": false,
"includeAll": false,
"current": {
"value": "order-api",
"text": "order-api"
}
}
]
},
"annotations": {
"list": [
{
"name": "Deployments",
"datasource": "Prometheus",
"expr": "changes(deployment_info{service=\"$service\"}[5m]) > 0",
"iconColor": "blue",
"enable": true
},
{
"name": "Incidents",
"datasource": "Prometheus",
"expr": "ALERTS{service=\"$service\",alertstate=\"firing\",severity=\"critical\"}",
"iconColor": "red",
"enable": true
}
]
}
}
}
{
"dashboard": {
"title": "Service Observability - Unified Dashboard",
"description": "Unified observability dashboard combining logs, metrics, and traces",
"tags": ["observability", "production", "unified"],
"timezone": "browser",
"refresh": "30s",
"time": {
"from": "now-1h",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Request Rate (req/s)",
"type": "graph",
"gridPos": { "x": 0, "y": 0, "w": 8, "h": 6 },
"targets": [
{
"expr": "sum(rate(http_requests_total{service=\"$service\"}[5m])) by (status)",
"legendFormat": "{{status}}"
}
],
"yaxes": [
{
"format": "reqps",
"min": 0
}
]
},
{
"id": 2,
"title": "Error Rate (%)",
"type": "graph",
"gridPos": { "x": 8, "y": 0, "w": 8, "h": 6 },
"targets": [
{
"expr": "(sum(rate(http_requests_total{service=\"$service\",status=~\"5..\"}[5m])) / sum(rate(http_requests_total{service=\"$service\"}[5m]))) * 100",
"legendFormat": "Error Rate"
}
],
"yaxes": [
{
"format": "percent",
"min": 0,
"max": 100
}
],
"alert": {
"conditions": [
{
"evaluator": {
"params": [5],
"type": "gt"
},
"query": {
"params": ["A", "5m", "now"]
},
"type": "query"
}
],
"frequency": "1m",
"handler": 1,
"message": "Error rate above 5%",
"name": "High Error Rate"
}
},
{
"id": 3,
"title": "Latency (P50, P95, P99)",
"type": "graph",
"gridPos": { "x": 16, "y": 0, "w": 8, "h": 6 },
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P50"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P95"
},
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m]))",
"legendFormat": "P99"
}
],
"yaxes": [
{
"format": "s",
"min": 0
}
]
},
{
"id": 4,
"title": "Active Traces (last 5m)",
"type": "stat",
"gridPos": { "x": 0, "y": 6, "w": 4, "h": 4 },
"targets": [
{
"expr": "sum(increase(traces_total{service=\"$service\"}[5m]))",
"legendFormat": "Traces"
}
],
"options": {
"colorMode": "value",
"graphMode": "area"
}
},
{
"id": 5,
"title": "Error Logs (last 5m)",
"type": "stat",
"gridPos": { "x": 4, "y": 6, "w": 4, "h": 4 },
"targets": [
{
"expr": "sum(increase(log_messages_total{service=\"$service\",level=\"error\"}[5m]))",
"legendFormat": "Errors"
}
],
"options": {
"colorMode": "value",
"graphMode": "area"
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "green" },
{ "value": 10, "color": "yellow" },
{ "value": 50, "color": "red" }
]
}
}
}
},
{
"id": 6,
"title": "Active Alerts",
"type": "stat",
"gridPos": { "x": 8, "y": 6, "w": 4, "h": 4 },
"targets": [
{
"expr": "count(ALERTS{service=\"$service\",alertstate=\"firing\"})",
"legendFormat": "Firing"
}
],
"options": {
"colorMode": "background"
},
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "green" },
{ "value": 1, "color": "orange" },
{ "value": 3, "color": "red" }
]
}
}
}
},
{
"id": 7,
"title": "CPU Usage (%)",
"type": "gauge",
"gridPos": { "x": 12, "y": 6, "w": 6, "h": 4 },
"targets": [
{
"expr": "avg(rate(process_cpu_seconds_total{service=\"$service\"}[5m])) * 100",
"legendFormat": "CPU"
}
],
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "green" },
{ "value": 70, "color": "yellow" },
{ "value": 90, "color": "red" }
]
}
}
}
},
{
"id": 8,
"title": "Memory Usage (MB)",
"type": "gauge",
"gridPos": { "x": 18, "y": 6, "w": 6, "h": 4 },
"targets": [
{
"expr": "avg(process_resident_memory_bytes{service=\"$service\"}) / 1024 / 1024",
"legendFormat": "Memory"
}
],
"fieldConfig": {
"defaults": {
"unit": "decmbytes",
"thresholds": {
"mode": "absolute",
"steps": [
{ "value": 0, "color": "green" },
{ "value": 400, "color": "yellow" },
{ "value": 800, "color": "red" }
]
}
}
}
},
{
"id": 9,
"title": "Recent Error Logs",
"type": "logs",
"gridPos": { "x": 0, "y": 10, "w": 12, "h": 8 },
"targets": [
{
"expr": "{service=\"$service\", level=\"error\"}",
"refId": "A"
}
],
"options": {
"showTime": true,
"showLabels": true,
"sortOrder": "Descending",
"wrapLogMessage": true
}
},
{
"id": 10,
"title": "Slowest Traces (P99)",
"type": "table",
"gridPos": { "x": 12, "y": 10, "w": 12, "h": 8 },
"targets": [
{
"expr": "topk(10, histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{service=\"$service\"}[5m])) by (endpoint))",
"format": "table",
"instant": true
}
],
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"renameByName": {
"endpoint": "Endpoint",
"Value": "P99 Latency (s)"
}
}
}
]
},
{
"id": 11,
"title": "Trace Waterfall (Click to explore)",
"type": "traces",
"gridPos": { "x": 0, "y": 18, "w": 24, "h": 10 },
"datasource": "Jaeger",
"targets": [
{
"query": "service=\"$service\"",
"refId": "A"
}
],
"options": {
"showSpanDetails": true,
"showServiceGraph": true
}
}
],
"templating": {
"list": [
{
"name": "service",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(http_requests_total, service)",
"multi": false,
"includeAll": false,
"current": {
"value": "order-api",
"text": "order-api"
}
},
{
"name": "environment",
"type": "query",
"datasource": "Prometheus",
"query": "label_values(http_requests_total{service=\"$service\"}, environment)",
"multi": false,
"includeAll": false
}
]
},
"annotations": {
"list": [
{
"name": "Deployments",
"datasource": "Prometheus",
"expr": "changes(deployment_info{service=\"$service\"}[5m]) > 0",
"iconColor": "blue",
"enable": true,
"tagKeys": "version"
},
{
"name": "Incidents",
"datasource": "Prometheus",
"expr": "ALERTS{service=\"$service\",alertstate=\"firing\",severity=\"critical\"}",
"iconColor": "red",
"enable": true
},
{
"name": "Slow Requests",
"datasource": "Jaeger",
"query": "service=\"$service\" AND duration > 1s",
"iconColor": "orange",
"enable": true
}
]
},
"links": [
{
"title": "Jaeger - Trace Explorer",
"url": "http://jaeger:16686/search?service=$service",
"type": "link",
"icon": "external link"
},
{
"title": "Prometheus - Service Metrics",
"url": "http://prometheus:9090/graph?g0.expr=http_requests_total%7Bservice%3D%22$service%22%7D",
"type": "link",
"icon": "external link"
},
{
"title": "Loki - Service Logs",
"url": "http://loki:3100/loki/api/v1/query_range?query=%7Bservice%3D%22$service%22%7D",
"type": "link",
"icon": "external link"
}
]
}
}
# Prometheus Alert Rules for SLO Monitoring
#
# This template provides production-ready alert rules based on Google SRE practices.
# Deploy to Prometheus via ConfigMap or rules file.
groups:
# ============================================================
# SLO Burn Rate Alerts (Multi-Window)
# Based on Google SRE Workbook Chapter 5
# ============================================================
- name: slo-burn-rate-alerts
interval: 30s
rules:
# Fast Burn - Critical (2% budget in 1 hour)
- alert: ErrorBudgetBurnRateCritical1h
expr: |
(
1 - (
sum(rate(http_requests_total{service="$SERVICE",status!~"5.."}[1h]))
/
sum(rate(http_requests_total{service="$SERVICE"}[1h]))
)
) > (14.4 * (1 - $SLO_TARGET))
for: 2m
labels:
severity: critical
slo: availability
burn_rate: fast
annotations:
summary: "Critical error budget burn rate for {{ $labels.service }}"
description: |
Service {{ $labels.service }} is consuming 2% of 30-day error budget in 1 hour.
Current error rate: {{ $value | humanizePercentage }}
At this rate, entire error budget will be exhausted in 50 hours.
runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
dashboard_url: "https://grafana.example.com/d/slo/{{ $labels.service }}"
# Fast Burn - Warning (5% budget in 6 hours)
- alert: ErrorBudgetBurnRateWarning6h
expr: |
(
1 - (
sum(rate(http_requests_total{service="$SERVICE",status!~"5.."}[6h]))
/
sum(rate(http_requests_total{service="$SERVICE"}[6h]))
)
) > (6 * (1 - $SLO_TARGET))
for: 15m
labels:
severity: warning
slo: availability
burn_rate: fast
annotations:
summary: "Warning error budget burn rate for {{ $labels.service }}"
description: |
Service {{ $labels.service }} is consuming 5% of 30-day error budget in 6 hours.
Current error rate: {{ $value | humanizePercentage }}
At this rate, entire error budget will be exhausted in 5 days.
runbook_url: "https://wiki.example.com/runbooks/elevated-error-rate"
# Slow Burn - Warning (10% budget in 1 day)
- alert: ErrorBudgetBurnRateWarning1d
expr: |
(
1 - (
sum(rate(http_requests_total{service="$SERVICE",status!~"5.."}[1d]))
/
sum(rate(http_requests_total{service="$SERVICE"}[1d]))
)
) > (3 * (1 - $SLO_TARGET))
for: 1h
labels:
severity: warning
slo: availability
burn_rate: slow
annotations:
summary: "Slow error budget burn for {{ $labels.service }}"
description: |
Service {{ $labels.service }} is consuming 10% of 30-day error budget in 1 day.
Current error rate: {{ $value | humanizePercentage }}
At this rate, entire error budget will be exhausted in 10 days.
# Slow Burn - Info (10% budget in 3 days)
- alert: ErrorBudgetBurnRateInfo3d
expr: |
(
1 - (
sum(rate(http_requests_total{service="$SERVICE",status!~"5.."}[3d]))
/
sum(rate(http_requests_total{service="$SERVICE"}[3d]))
)
) > (1 * (1 - $SLO_TARGET))
for: 6h
labels:
severity: info
slo: availability
burn_rate: slow
annotations:
summary: "Error budget consumption for {{ $labels.service }}"
description: |
Service {{ $labels.service }} is on track to consume entire error budget by end of window.
Current error rate: {{ $value | humanizePercentage }}
# ============================================================
# Latency SLO Alerts
# ============================================================
- name: latency-slo-alerts
interval: 30s
rules:
- alert: LatencyP99Degradation
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{service="$SERVICE"}[5m])
) > $LATENCY_TARGET_SECONDS
for: 10m
labels:
severity: warning
slo: latency
annotations:
summary: "P99 latency above target for {{ $labels.service }}"
description: |
P99 latency is {{ $value | humanizeDuration }}, target is {{ $LATENCY_TARGET_SECONDS }}s
Service: {{ $labels.service }}
Endpoint: {{ $labels.endpoint }}
runbook_url: "https://wiki.example.com/runbooks/high-latency"
- alert: LatencyP99Critical
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{service="$SERVICE"}[5m])
) > ($LATENCY_TARGET_SECONDS * 2)
for: 5m
labels:
severity: critical
slo: latency
annotations:
summary: "P99 latency critically high for {{ $labels.service }}"
description: |
P99 latency is {{ $value | humanizeDuration }}, 2x the target of {{ $LATENCY_TARGET_SECONDS }}s
Immediate investigation required.
# ============================================================
# Error Budget Exhaustion Alerts
# ============================================================
- name: error-budget-alerts
interval: 5m
rules:
- alert: ErrorBudgetNearlyExhausted
expr: |
(
(($SLO_TARGET) - (
1 - (
sum(rate(http_requests_total{service="$SERVICE",status!~"5.."}[30d]))
/
sum(rate(http_requests_total{service="$SERVICE"}[30d]))
)
)) / (1 - $SLO_TARGET)
) * 100 < 10
for: 30m
labels:
severity: warning
annotations:
summary: "Error budget nearly exhausted for {{ $labels.service }}"
description: |
Only {{ $value | humanize }}% of error budget remaining for {{ $labels.service }}.
Consider feature freeze to preserve reliability.
- alert: ErrorBudgetExhausted
expr: |
(
(($SLO_TARGET) - (
1 - (
sum(rate(http_requests_total{service="$SERVICE",status!~"5.."}[30d]))
/
sum(rate(http_requests_total{service="$SERVICE"}[30d]))
)
)) / (1 - $SLO_TARGET)
) * 100 <= 0
for: 15m
labels:
severity: critical
annotations:
summary: "Error budget exhausted for {{ $labels.service }}"
description: |
Error budget is EXHAUSTED for {{ $labels.service }}.
Immediate action required:
- Stop all feature releases
- Focus on reliability improvements
- Incident response mode
# ============================================================
# Traffic Anomaly Detection
# ============================================================
- name: traffic-anomalies
interval: 1m
rules:
- alert: TrafficDropSignificant
expr: |
(
sum(rate(http_requests_total{service="$SERVICE"}[5m]))
/
sum(rate(http_requests_total{service="$SERVICE"}[5m] offset 1h))
) < 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "Traffic dropped significantly for {{ $labels.service }}"
description: |
Current traffic is 50% below traffic 1 hour ago.
This may indicate an outage or upstream issue.
Current: {{ $value | humanize }} req/s
- alert: TrafficSpikeUnexpected
expr: |
(
sum(rate(http_requests_total{service="$SERVICE"}[5m]))
/
sum(rate(http_requests_total{service="$SERVICE"}[5m] offset 1h))
) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Unexpected traffic spike for {{ $labels.service }}"
description: |
Current traffic is 3x higher than 1 hour ago.
Current: {{ $value | humanize }} req/s
Check for DDoS, viral event, or misconfiguration.
# ============================================================
# Availability Alerts (Blackbox Monitoring)
# ============================================================
- name: availability-blackbox
interval: 30s
rules:
- alert: ServiceDown
expr: up{job="$SERVICE"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Service {{ $labels.service }} is down"
description: |
Service {{ $labels.instance }} has been down for more than 1 minute.
Target: {{ $labels.instance }}
- alert: HealthCheckFailing
expr: probe_success{job="$SERVICE-health"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Health check failing for {{ $labels.service }}"
description: |
Health check for {{ $labels.instance }} has been failing for 2 minutes.
Check application logs and resource usage.
# ============================================================
# Usage Examples
# ============================================================
# 1. Deploy to Kubernetes via ConfigMap:
#
# kubectl create configmap prometheus-rules \
# --from-file=slo-alerts.yaml \
# --namespace=monitoring
#
# 2. Set variables in prometheus.yaml:
#
# global:
# external_labels:
# SERVICE: "order-api"
# SLO_TARGET: "0.999"
# LATENCY_TARGET_SECONDS: "0.5"
#
# 3. Test alerts:
#
# promtool test rules slo-alerts.yaml
#
# 4. Validate in Prometheus UI:
#
# http://prometheus:9090/alerts
# ============================================================
# Common SLO Targets
# ============================================================
# Service Type | Availability SLO | Latency SLO (P99)
# --------------------------|------------------|-------------------
# User-facing API | 99.9% (0.999) | 500ms (0.5s)
# Internal API | 99.5% (0.995) | 1s
# Batch processing | 99% (0.99) | N/A
# Critical payment service | 99.99% (0.9999) | 200ms (0.2s)
# SLO Definition Template
#
# Use this template to define Service Level Objectives (SLOs) for your services.
# Based on Google SRE best practices.
---
# Service: Order API
service:
name: order-api
description: Core API for order processing
team: platform-team
oncall: platform-oncall@company.com
# SLOs
slos:
# Availability SLO
- name: order-api-availability
objective: "99.9% of order API requests succeed over 30 days"
sli:
type: availability
measurement: |
sum(rate(http_requests_total{service="order-api",status!~"5.."}[30d]))
/
sum(rate(http_requests_total{service="order-api"}[30d]))
datasource: prometheus
target: 0.999 # 99.9%
window: 30d
error_budget:
allowed_failure_rate: 0.1% # 0.001
allowed_downtime_minutes: 43.2 # per 30 days
burn_rate_alerts:
- name: critical-burn-1h
window: 1h
burn_rate: 14.4
budget_consumed: 2%
severity: critical
notification: pagerduty
- name: critical-burn-6h
window: 6h
burn_rate: 6
budget_consumed: 5%
severity: critical
notification: slack
- name: warning-burn-1d
window: 1d
burn_rate: 3
budget_consumed: 10%
severity: warning
notification: slack
error_budget_policy:
- threshold: 50%
action: full_velocity
description: "Ship all features, run experiments"
- threshold: 25%
action: reduced_velocity
description: "Critical features only, increase test coverage"
- threshold: 10%
action: feature_freeze
description: "Stop feature releases, focus on reliability"
- threshold: 0%
action: incident_mode
description: "Emergency response, stop all releases"
# Latency SLO
- name: order-api-latency-p99
objective: "99% of order API requests complete in < 500ms over 30 days"
sli:
type: latency
measurement: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{service="order-api"}[30d])
)
datasource: prometheus
target: 0.5 # 500ms
window: 30d
error_budget:
allowed_slow_requests: 1%
burn_rate_alerts:
- name: latency-degradation
window: 1h
threshold: 1000 # P99 > 1s for 1 hour
severity: warning
notification: slack
# User Journey SLO (Checkout Flow)
- name: checkout-success-rate
objective: "95% of checkout flows complete successfully over 7 days"
sli:
type: success_rate
measurement: |
sum(rate(checkout_completed_total{service="order-api"}[7d]))
/
sum(rate(checkout_started_total{service="order-api"}[7d]))
datasource: prometheus
target: 0.95 # 95%
window: 7d
error_budget:
allowed_failure_rate: 5%
burn_rate_alerts:
- name: checkout-failure-spike
window: 1h
burn_rate: 10
severity: critical
notification: pagerduty
# Prometheus Alert Rules
prometheus_alerts:
groups:
- name: slo-burn-rate-order-api
interval: 30s
rules:
# Fast burn (1 hour window)
- alert: OrderAPIErrorBudgetBurnRateCritical1h
expr: |
(
1 - (
sum(rate(http_requests_total{service="order-api",status!~"5.."}[1h]))
/
sum(rate(http_requests_total{service="order-api"}[1h]))
)
) > (14.4 * (1 - 0.999))
for: 2m
labels:
severity: critical
service: order-api
slo: availability
annotations:
summary: "Critical error budget burn rate for Order API (1h window)"
description: "Consuming 2% of 30-day error budget in 1 hour. Current error rate {{ $value | humanizePercentage }}"
runbook_url: "https://wiki.company.com/runbooks/order-api-high-error-rate"
# Slow burn (6 hour window)
- alert: OrderAPIErrorBudgetBurnRateWarning6h
expr: |
(
1 - (
sum(rate(http_requests_total{service="order-api",status!~"5.."}[6h]))
/
sum(rate(http_requests_total{service="order-api"}[6h]))
)
) > (6 * (1 - 0.999))
for: 15m
labels:
severity: warning
service: order-api
slo: availability
annotations:
summary: "Warning error budget burn rate for Order API (6h window)"
description: "Consuming 5% of 30-day error budget in 6 hours. Current error rate {{ $value | humanizePercentage }}"
runbook_url: "https://wiki.company.com/runbooks/order-api-high-error-rate"
# Latency SLO
- alert: OrderAPILatencyDegradation
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{service="order-api"}[1h])
) > 1.0
for: 5m
labels:
severity: warning
service: order-api
slo: latency
annotations:
summary: "Order API P99 latency above 1s"
description: "P99 latency is {{ $value | humanizeDuration }}, target is 500ms"
runbook_url: "https://wiki.company.com/runbooks/order-api-slow-requests"
# Dashboard Configuration
grafana_dashboard:
title: "Order API - SLO Dashboard"
panels:
- title: "Availability SLI (30d)"
type: gauge
query: |
sum(rate(http_requests_total{service="order-api",status!~"5.."}[30d]))
/
sum(rate(http_requests_total{service="order-api"}[30d]))
thresholds:
- value: 0.999
color: green
- value: 0.995
color: yellow
- value: 0.99
color: red
- title: "Error Budget Remaining"
type: gauge
query: |
(
(0.999 - (
1 - (
sum(rate(http_requests_total{service="order-api",status!~"5.."}[30d]))
/
sum(rate(http_requests_total{service="order-api"}[30d]))
)
)) / 0.001
) * 100
thresholds:
- value: 50
color: green
- value: 25
color: yellow
- value: 10
color: red
- title: "Error Budget Burn Rate (1h)"
type: graph
query: |
(
1 - (
sum(rate(http_requests_total{service="order-api",status!~"5.."}[1h]))
/
sum(rate(http_requests_total{service="order-api"}[1h]))
)
) / (1 - 0.999)
- title: "P99 Latency (30d)"
type: graph
query: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket{service="order-api"}[5m])
)
thresholds:
- value: 0.5
color: green
- value: 1.0
color: red
- title: "SLI History (30d)"
type: graph
queries:
- name: "Target (99.9%)"
query: "0.999"
color: green
- name: "Actual SLI"
query: |
sum(rate(http_requests_total{service="order-api",status!~"5.."}[1h]))
/
sum(rate(http_requests_total{service="order-api"}[1h]))
color: blue
# Review Schedule
review:
frequency: weekly
participants:
- platform-team
- product-team
agenda:
- Review current SLI status
- Analyze error budget consumption
- Discuss incidents and postmortems
- Adjust SLO targets if needed
---
# Additional Examples
# Example: Payment Service SLO
# service:
# name: payment-service
# slos:
# - name: payment-success-rate
# objective: "99.95% of payment attempts succeed"
# sli:
# type: success_rate
# measurement: |
# sum(rate(payment_success_total[30d]))
# /
# sum(rate(payment_attempts_total[30d]))
# target: 0.9995
# window: 30d
# Example: Batch Job SLO
# service:
# name: data-pipeline
# slos:
# - name: batch-job-success-rate
# objective: "99% of daily batch jobs complete successfully"
# sli:
# type: success_rate
# measurement: |
# sum(rate(batch_job_success_total[7d]))
# /
# sum(rate(batch_job_total[7d]))
# target: 0.99
# window: 7d
# Example: Multi-region SLO
# service:
# name: global-api
# slos:
# - name: global-availability
# objective: "99.99% availability across all regions"
# sli:
# type: availability
# measurement: |
# sum(rate(http_requests_total{status!~"5.."}[30d])) by (region)
# /
# sum(rate(http_requests_total[30d])) by (region)
# target: 0.9999
# window: 30d
OpenTelemetry Node.js Setup Template
Complete setup template for instrumenting a Node.js application with OpenTelemetry.
Project Structure
my-app/
src/
instrumentation.js # OpenTelemetry setup (load FIRST)
server.js # Express server
routes/
orders.js
services/
orderService.js
package.json
.env1. Install Dependencies
npm install --save \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http \
@opentelemetry/sdk-metrics \
@opentelemetry/resources \
@opentelemetry/semantic-conventions \
@opentelemetry/api2. Create Instrumentation File
`src/instrumentation.js` (load BEFORE any other imports):
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { ParentBasedSampler, TraceIdRatioBasedSampler } = require('@opentelemetry/sdk-trace-base');
// Resource attributes (service metadata)
const resource = new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'my-service',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.SERVICE_VERSION || '1.0.0',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
});
// Sampling strategy
const sampler = new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(
process.env.NODE_ENV === 'production' ? 0.1 : 1.0 // 10% in prod, 100% in dev
),
});
// Initialize OpenTelemetry SDK
const sdk = new NodeSDK({
resource: resource,
sampler: sampler,
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/metrics',
}),
exportIntervalMillis: 60000, // Export every 60 seconds
}),
instrumentations: [
getNodeAutoInstrumentations({
// Disable noisy instrumentations
'@opentelemetry/instrumentation-fs': { enabled: false },
'@opentelemetry/instrumentation-net': { enabled: false },
'@opentelemetry/instrumentation-dns': { enabled: false },
}),
],
});
// Start SDK
sdk.start();
// Graceful shutdown
process.on('SIGTERM', () => {
sdk
.shutdown()
.then(() => console.log('OpenTelemetry shut down successfully'))
.catch((error) => console.error('Error shutting down OpenTelemetry', error))
.finally(() => process.exit(0));
});
module.exports = sdk;---
3. Update Server Entry Point
`src/server.js` (load instrumentation FIRST):
// MUST BE FIRST LINE
require('./instrumentation');
// Now import your app
const express = require('express');
const { trace, context } = require('@opentelemetry/api');
const app = express();
app.use(express.json());
// Add request ID middleware
app.use((req, res, next) => {
req.id = Math.random().toString(36).substring(7);
next();
});
// Import routes
const orderRoutes = require('./routes/orders');
app.use('/api/orders', orderRoutes);
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});---
4. Add Custom Spans to Business Logic
`src/services/orderService.js`:
const { trace, SpanStatusCode } = require('@opentelemetry/api');
class OrderService {
async processOrder(orderId) {
const tracer = trace.getTracer('order-service');
const span = tracer.startSpan('process-order');
// Add attributes
span.setAttribute('order.id', orderId);
span.setAttribute('order.source', 'web');
try {
// Validate order
span.addEvent('validation-started');
const isValid = await this.validateOrder(orderId);
span.addEvent('validation-completed', { is_valid: isValid });
if (!isValid) {
span.setStatus({ code: SpanStatusCode.ERROR, message: 'Invalid order' });
throw new Error('Invalid order');
}
// Process payment
span.addEvent('payment-started');
const payment = await this.processPayment(orderId);
span.setAttribute('payment.id', payment.id);
span.setAttribute('payment.amount', payment.amount);
span.addEvent('payment-completed');
// Fulfill order
span.addEvent('fulfillment-started');
await this.fulfillOrder(orderId);
span.addEvent('fulfillment-completed');
span.setStatus({ code: SpanStatusCode.OK });
return { success: true, orderId, paymentId: payment.id };
} catch (error) {
// Record exception
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw error;
} finally {
span.end();
}
}
async validateOrder(orderId) {
// Validation logic
return true;
}
async processPayment(orderId) {
// Payment logic
return { id: 'pay_123', amount: 99.99 };
}
async fulfillOrder(orderId) {
// Fulfillment logic
}
}
module.exports = new OrderService();---
5. Route with Tracing
`src/routes/orders.js`:
const express = require('express');
const { trace } = require('@opentelemetry/api');
const orderService = require('../services/orderService');
const router = express.Router();
router.post('/', async (req, res) => {
const tracer = trace.getTracer('order-api');
const span = tracer.startSpan('POST /api/orders');
span.setAttribute('http.method', 'POST');
span.setAttribute('http.route', '/api/orders');
span.setAttribute('user.id', req.user?.id || 'anonymous');
try {
const order = await orderService.processOrder(req.body);
span.setAttribute('http.status_code', 201);
res.status(201).json(order);
} catch (error) {
span.recordException(error);
span.setAttribute('http.status_code', 500);
res.status(500).json({ error: error.message });
} finally {
span.end();
}
});
module.exports = router;---
6. Environment Variables
`.env`:
# Service metadata
SERVICE_NAME=order-api
SERVICE_VERSION=1.0.0
NODE_ENV=production
# OpenTelemetry exporter
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Application
PORT=3000---
7. Docker Compose (Local Development)
`docker-compose.yml`:
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4318:4318" # OTLP HTTP receiver
environment:
- COLLECTOR_OTLP_ENABLED=true
app:
build: .
ports:
- "3000:3000"
environment:
- SERVICE_NAME=order-api
- SERVICE_VERSION=1.0.0
- NODE_ENV=development
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
depends_on:
- jaeger---
8. Kubernetes Deployment
`k8s/deployment.yaml`:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
spec:
replicas: 3
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
spec:
containers:
- name: app
image: my-registry/order-api:1.0.0
ports:
- containerPort: 3000
env:
- name: SERVICE_NAME
value: "order-api"
- name: SERVICE_VERSION
value: "1.0.0"
- name: NODE_ENV
value: "production"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector:4318"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"---
9. Testing Instrumentation
Run application:
# Start Jaeger
docker-compose up -d jaeger
# Start app
npm start
# Make requests
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{"items": [{"id": 123, "quantity": 2}]}'
# View traces in Jaeger UI
open http://localhost:16686---
10. Debugging
Enable debug logging:
// Add to instrumentation.js (before SDK initialization)
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);Common issues:
1. No traces appearing:
- Check OTEL_EXPORTER_OTLP_ENDPOINT is reachable
- Verify instrumentation.js loads first
- Check sampling rate (set to 1.0 for testing)
2. Broken traces:
- Verify trace context propagation in HTTP calls
- Check that all services use same trace format
3. High overhead:
- Reduce sampling rate (0.01 = 1%)
- Disable unnecessary instrumentations
- Use BatchSpanProcessor (default)
---
Next Steps
1. Add structured logging with trace correlation 2. Set up metrics (custom counters, gauges, histograms) 3. Configure SLOs and alerting 4. Integrate with APM (Datadog, New Relic, etc.) 5. Add trace exemplars to metrics
Related topics:
- Structured Logging with trace correlation
- Prometheus Metrics configuration
- SLO Definition patterns
OpenTelemetry Python Setup Template
Complete setup template for instrumenting a Python/Flask application with OpenTelemetry.
Project Structure
my-app/
src/
instrumentation.py # OpenTelemetry setup
app.py # Flask application
services/
order_service.py
routes/
orders.py
requirements.txt
.env1. Install Dependencies
`requirements.txt`:
flask==3.0.0
opentelemetry-api==1.21.0
opentelemetry-sdk==1.21.0
opentelemetry-instrumentation-flask==0.42b0
opentelemetry-instrumentation-requests==0.42b0
opentelemetry-instrumentation-sqlalchemy==0.42b0
opentelemetry-exporter-otlp-proto-http==1.21.0
python-dotenv==1.0.0pip install -r requirements.txt---
2. Create Instrumentation File
`src/instrumentation.py`:
import os
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
# Resource attributes (service metadata)
resource = Resource.create({
"service.name": os.getenv("SERVICE_NAME", "my-service"),
"service.version": os.getenv("SERVICE_VERSION", "1.0.0"),
"deployment.environment": os.getenv("ENV", "development")
})
# Sampling strategy
sampling_rate = 0.1 if os.getenv("ENV") == "production" else 1.0
sampler = ParentBasedTraceIdRatio(sampling_rate)
# Configure tracing
trace_provider = TracerProvider(resource=resource, sampler=sampler)
trace_processor = BatchSpanProcessor(
OTLPSpanExporter(
endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
)
)
trace_provider.add_span_processor(trace_processor)
trace.set_tracer_provider(trace_provider)
# Configure metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(
endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/metrics")
),
export_interval_millis=60000 # Export every 60 seconds
)
metric_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(metric_provider)
# Graceful shutdown
import atexit
def shutdown_telemetry():
trace_provider.shutdown()
metric_provider.shutdown()
atexit.register(shutdown_telemetry)---
3. Create Flask Application
`src/app.py`:
from flask import Flask, request, jsonify
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry import trace
from dotenv import load_dotenv
import os
# Load environment variables
load_dotenv()
# Import instrumentation FIRST
from instrumentation import trace_provider
# Create Flask app
app = Flask(__name__)
# Instrument Flask (auto-traces all routes)
FlaskInstrumentor().instrument_app(app)
# Import routes
from routes.orders import orders_bp
app.register_blueprint(orders_bp, url_prefix='/api/orders')
# Health check
@app.route('/health')
def health():
return jsonify({"status": "ok"})
if __name__ == '__main__':
port = int(os.getenv('PORT', 3000))
app.run(host='0.0.0.0', port=port)---
4. Add Custom Spans to Business Logic
`src/services/order_service.py`:
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
class OrderService:
def __init__(self):
self.tracer = trace.get_tracer(__name__)
def process_order(self, order_id):
with self.tracer.start_as_current_span("process-order") as span:
# Add attributes
span.set_attribute("order.id", order_id)
span.set_attribute("order.source", "web")
try:
# Validate order
span.add_event("validation-started")
is_valid = self._validate_order(order_id)
span.add_event("validation-completed", {"is_valid": is_valid})
if not is_valid:
span.set_status(Status(StatusCode.ERROR, "Invalid order"))
raise ValueError("Invalid order")
# Process payment
span.add_event("payment-started")
payment = self._process_payment(order_id)
span.set_attribute("payment.id", payment["id"])
span.set_attribute("payment.amount", payment["amount"])
span.add_event("payment-completed")
# Fulfill order
span.add_event("fulfillment-started")
self._fulfill_order(order_id)
span.add_event("fulfillment-completed")
span.set_status(Status(StatusCode.OK))
return {
"success": True,
"order_id": order_id,
"payment_id": payment["id"]
}
except Exception as error:
# Record exception
span.record_exception(error)
span.set_status(Status(StatusCode.ERROR, str(error)))
raise
def _validate_order(self, order_id):
# Validation logic
return True
def _process_payment(self, order_id):
# Payment logic
return {"id": "pay_123", "amount": 99.99}
def _fulfill_order(self, order_id):
# Fulfillment logic
pass
order_service = OrderService()---
5. Route with Tracing
`src/routes/orders.py`:
from flask import Blueprint, request, jsonify
from opentelemetry import trace
from services.order_service import order_service
orders_bp = Blueprint('orders', __name__)
tracer = trace.get_tracer(__name__)
@orders_bp.route('/', methods=['POST'])
def create_order():
with tracer.start_as_current_span("POST /api/orders") as span:
span.set_attribute("http.method", "POST")
span.set_attribute("http.route", "/api/orders")
span.set_attribute("user.id", request.headers.get("X-User-ID", "anonymous"))
try:
data = request.get_json()
order = order_service.process_order(data.get("order_id"))
span.set_attribute("http.status_code", 201)
return jsonify(order), 201
except Exception as error:
span.record_exception(error)
span.set_attribute("http.status_code", 500)
return jsonify({"error": str(error)}), 500---
6. Environment Variables
`.env`:
# Service metadata
SERVICE_NAME=order-api
SERVICE_VERSION=1.0.0
ENV=production
# OpenTelemetry exporter
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Application
PORT=3000---
7. Docker Compose (Local Development)
`docker-compose.yml`:
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4318:4318" # OTLP HTTP receiver
environment:
- COLLECTOR_OTLP_ENABLED=true
app:
build: .
ports:
- "3000:3000"
environment:
- SERVICE_NAME=order-api
- SERVICE_VERSION=1.0.0
- ENV=development
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
depends_on:
- jaeger`Dockerfile`:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ ./src/
CMD ["python", "src/app.py"]---
8. Kubernetes Deployment
`k8s/deployment.yaml`:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
spec:
replicas: 3
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
spec:
containers:
- name: app
image: my-registry/order-api:1.0.0
ports:
- containerPort: 3000
env:
- name: SERVICE_NAME
value: "order-api"
- name: SERVICE_VERSION
value: "1.0.0"
- name: ENV
value: "production"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector:4318"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"---
9. Testing Instrumentation
Run application:
# Start Jaeger
docker-compose up -d jaeger
# Start app
python src/app.py
# Make requests
curl -X POST http://localhost:3000/api/orders \
-H "Content-Type: application/json" \
-d '{"order_id": "order-123"}'
# View traces in Jaeger UI
open http://localhost:16686---
10. Advanced: Database Instrumentation
Install SQLAlchemy instrumentation:
pip install opentelemetry-instrumentation-sqlalchemy`src/database.py`:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
# Create engine
engine = create_engine('postgresql://user:password@localhost/dbname')
# Instrument SQLAlchemy
SQLAlchemyInstrumentor().instrument(engine=engine)
# Create session
Session = sessionmaker(bind=engine)Usage:
from database import Session
with tracer.start_as_current_span("fetch-orders"):
session = Session()
orders = session.query(Order).filter_by(status='pending').all()
# Automatically traced!---
11. Debugging
Enable debug logging:
# Add to instrumentation.py
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("opentelemetry").setLevel(logging.DEBUG)Common issues:
1. No traces appearing:
- Check OTEL_EXPORTER_OTLP_ENDPOINT is reachable
- Verify instrumentation.py imports before Flask
- Check sampling rate (set to 1.0 for testing)
2. Import errors:
- Ensure all opentelemetry packages are installed
- Check version compatibility
3. High overhead:
- Reduce sampling rate (0.01 = 1%)
- Use BatchSpanProcessor (default)
---
Next Steps
1. Add structured logging with trace correlation 2. Set up metrics (custom counters, gauges, histograms) 3. Configure SLOs and alerting 4. Integrate with APM (Datadog, New Relic, etc.)
Related topics:
- Structured Logging with trace correlation
- Prometheus Metrics configuration
- SLO Definition patterns
// Node.js Performance Profiling Configuration Template
//
// Use with clinic.js for comprehensive profiling
// Install: npm install -g clinic
module.exports = {
// ============================================================
// Clinic.js Configuration
// ============================================================
clinic: {
// CPU profiling (flame graph)
flame: {
command: 'node app.js',
sampleInterval: 10, // ms
detectPort: true,
debug: false
},
// Event loop delay profiling
doctor: {
command: 'node app.js',
sampleInterval: 10,
detectPort: true,
debug: false
},
// Async operations profiling
bubbleprof: {
command: 'node app.js',
detectPort: true,
debug: false
}
},
// ============================================================
// V8 Profiler Configuration
// ============================================================
v8: {
// CPU profiling
cpuProfile: {
enabled: process.env.NODE_ENV !== 'production',
interval: 1000, // Sampling interval in microseconds
outputPath: './profiles/cpu-profile-${Date.now()}.cpuprofile'
},
// Heap snapshot
heapSnapshot: {
enabled: true,
interval: 60 * 60 * 1000, // Every hour
outputPath: './profiles/heap-${Date.now()}.heapsnapshot',
maxSnapshots: 5 // Keep last 5 snapshots
},
// Heap statistics
heapStats: {
enabled: true,
interval: 10000, // Every 10 seconds
thresholds: {
heapUsedMB: 800, // Alert if heap usage > 800MB
externalMB: 100, // Alert if external memory > 100MB
arrayBuffersMB: 50 // Alert if array buffers > 50MB
}
}
},
// ============================================================
// Performance Monitoring
// ============================================================
monitoring: {
// Event loop lag monitoring
eventLoop: {
enabled: true,
warningThreshold: 50, // ms
criticalThreshold: 100 // ms
},
// GC monitoring
gc: {
enabled: true,
logSlowGC: true,
slowGCThreshold: 100 // ms
},
// Memory leak detection
memoryLeak: {
enabled: true,
checkInterval: 60000, // 1 minute
growthThreshold: 10, // Alert if heap grows for 10 consecutive checks
action: 'snapshot' // 'snapshot' | 'alert' | 'restart'
}
},
// ============================================================
// APM Integration (Example: Datadog)
// ============================================================
apm: {
enabled: process.env.NODE_ENV === 'production',
provider: 'datadog', // 'datadog' | 'newrelic' | 'elastic'
config: {
service: process.env.SERVICE_NAME || 'my-service',
env: process.env.NODE_ENV || 'development',
version: process.env.SERVICE_VERSION || '1.0.0',
// Profiling
profiling: true,
runtimeMetrics: true,
// Sampling
sampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0
}
}
};
// ============================================================
// Profiling Setup Functions
// ============================================================
function setupCPUProfiling() {
const v8 = require('v8');
const fs = require('fs');
const path = require('path');
const config = module.exports.v8.cpuProfile;
if (!config.enabled) return;
// Create profiles directory
const profilesDir = path.dirname(config.outputPath);
if (!fs.existsSync(profilesDir)) {
fs.mkdirSync(profilesDir, { recursive: true });
}
// Start profiling on SIGUSR1
process.on('SIGUSR1', () => {
const startTime = Date.now();
const outputPath = config.outputPath.replace('${Date.now()}', startTime);
console.log(`Starting CPU profile: ${outputPath}`);
// Note: Requires --prof flag or inspector API
// This is a simplified example
console.log('Use: node --prof app.js');
console.log('Then: node --prof-process isolate-*.log > profile.txt');
});
}
function setupHeapSnapshots() {
const v8 = require('v8');
const fs = require('fs');
const path = require('path');
const config = module.exports.v8.heapSnapshot;
if (!config.enabled) return;
// Create profiles directory
const profilesDir = path.dirname(config.outputPath);
if (!fs.existsSync(profilesDir)) {
fs.mkdirSync(profilesDir, { recursive: true });
}
function takeSnapshot() {
const outputPath = config.outputPath.replace('${Date.now()}', Date.now());
console.log(`Taking heap snapshot: ${outputPath}`);
v8.writeHeapSnapshot(outputPath);
// Cleanup old snapshots
cleanupOldSnapshots(profilesDir, config.maxSnapshots);
}
// Take snapshot on SIGUSR2
process.on('SIGUSR2', takeSnapshot);
// Periodic snapshots
if (config.interval) {
setInterval(takeSnapshot, config.interval);
}
}
function cleanupOldSnapshots(dir, maxSnapshots) {
const fs = require('fs');
const path = require('path');
const files = fs.readdirSync(dir)
.filter(f => f.endsWith('.heapsnapshot'))
.map(f => ({
name: f,
path: path.join(dir, f),
time: fs.statSync(path.join(dir, f)).mtime.getTime()
}))
.sort((a, b) => b.time - a.time);
// Delete old files
files.slice(maxSnapshots).forEach(file => {
fs.unlinkSync(file.path);
console.log(`Deleted old snapshot: ${file.name}`);
});
}
function setupHeapMonitoring() {
const config = module.exports.v8.heapStats;
if (!config.enabled) return;
setInterval(() => {
const usage = process.memoryUsage();
const heapUsedMB = Math.round(usage.heapUsed / 1024 / 1024);
const externalMB = Math.round(usage.external / 1024 / 1024);
const arrayBuffersMB = Math.round(usage.arrayBuffers / 1024 / 1024);
console.log({
heapUsedMB,
heapTotalMB: Math.round(usage.heapTotal / 1024 / 1024),
externalMB,
arrayBuffersMB,
rssMB: Math.round(usage.rss / 1024 / 1024)
});
// Alert on threshold breach
if (heapUsedMB > config.thresholds.heapUsedMB) {
console.error(`Heap usage above threshold: ${heapUsedMB}MB > ${config.thresholds.heapUsedMB}MB`);
}
if (externalMB > config.thresholds.externalMB) {
console.error(`External memory above threshold: ${externalMB}MB > ${config.thresholds.externalMB}MB`);
}
if (arrayBuffersMB > config.thresholds.arrayBuffersMB) {
console.error(`Array buffers above threshold: ${arrayBuffersMB}MB > ${config.thresholds.arrayBuffersMB}MB`);
}
}, config.interval);
}
function setupEventLoopMonitoring() {
const config = module.exports.monitoring.eventLoop;
if (!config.enabled) return;
let lastCheck = Date.now();
setInterval(() => {
const now = Date.now();
const lag = now - lastCheck - 100; // 100ms is the interval
if (lag > config.criticalThreshold) {
console.error(`Event loop lag CRITICAL: ${lag}ms`);
} else if (lag > config.warningThreshold) {
console.warn(`Event loop lag WARNING: ${lag}ms`);
}
lastCheck = now;
}, 100);
}
function setupMemoryLeakDetection() {
const config = module.exports.monitoring.memoryLeak;
if (!config.enabled) return;
let lastHeapUsed = 0;
let growthCount = 0;
setInterval(() => {
const currentHeapUsed = process.memoryUsage().heapUsed;
if (currentHeapUsed > lastHeapUsed) {
growthCount++;
if (growthCount >= config.growthThreshold) {
console.error(`Possible memory leak detected (heap growing for ${growthCount} checks)`);
if (config.action === 'snapshot') {
const v8 = require('v8');
const filename = `./profiles/leak-${Date.now()}.heapsnapshot`;
v8.writeHeapSnapshot(filename);
console.log(`Heap snapshot taken: ${filename}`);
}
growthCount = 0;
}
} else {
growthCount = 0;
}
lastHeapUsed = currentHeapUsed;
}, config.checkInterval);
}
// ============================================================
// Initialize Profiling
// ============================================================
function initializeProfiling() {
console.log('Initializing performance profiling...');
setupCPUProfiling();
setupHeapSnapshots();
setupHeapMonitoring();
setupEventLoopMonitoring();
setupMemoryLeakDetection();
console.log('Profiling enabled. Send SIGUSR1 for CPU profile, SIGUSR2 for heap snapshot.');
}
// Auto-initialize if required directly
if (require.main === module) {
initializeProfiling();
}
module.exports.initializeProfiling = initializeProfiling;
// ============================================================
// Usage Examples
// ============================================================
/*
1. Enable profiling in application:
const profiling = require('./profiling-config');
profiling.initializeProfiling();
2. Take heap snapshot:
kill -SIGUSR2 $(pgrep -f "node app.js")
3. Run with clinic.js:
clinic doctor -- node app.js
clinic flame -- node app.js
clinic bubbleprof -- node app.js
4. Built-in profiler:
node --prof app.js
node --prof-process isolate-*.log > profile.txt
5. Chrome DevTools:
node --inspect app.js
# Open chrome://inspect
*/
{
"ci": {
"collect": {
"url": [
"http://localhost:3000/",
"http://localhost:3000/products",
"http://localhost:3000/checkout"
],
"numberOfRuns": 3,
"settings": {
"preset": "desktop",
"throttling": {
"rttMs": 40,
"throughputKbps": 10240,
"cpuSlowdownMultiplier": 1
}
}
},
"assert": {
"preset": "lighthouse:recommended",
"assertions": {
"categories:performance": ["error", {"minScore": 0.9}],
"categories:accessibility": ["warn", {"minScore": 0.9}],
"categories:best-practices": ["warn", {"minScore": 0.9}],
"categories:seo": ["warn", {"minScore": 0.9}],
"first-contentful-paint": ["error", {"maxNumericValue": 2000}],
"largest-contentful-paint": ["error", {"maxNumericValue": 2500}],
"cumulative-layout-shift": ["error", {"maxNumericValue": 0.1}],
"total-blocking-time": ["error", {"maxNumericValue": 300}],
"speed-index": ["error", {"maxNumericValue": 3500}],
"resource-summary:script:size": ["error", {"maxNumericValue": 300000}],
"resource-summary:image:size": ["error", {"maxNumericValue": 500000}],
"resource-summary:total:size": ["error", {"maxNumericValue": 1000000}],
"uses-optimized-images": "warn",
"uses-text-compression": "error",
"uses-responsive-images": "warn",
"offscreen-images": "warn",
"render-blocking-resources": "warn",
"unminified-css": "error",
"unminified-javascript": "error",
"unused-css-rules": "warn",
"unused-javascript": "warn",
"modern-image-formats": "warn",
"uses-long-cache-ttl": "warn",
"total-byte-weight": "warn"
}
},
"upload": {
"target": "temporary-public-storage"
},
"server": {
"port": 9001,
"storage": {
"storageMethod": "sql",
"sqlDatabasePath": "./lhci.db"
}
}
}
}
{
"metadata": {
"skill": "qa-observability",
"updated": "2026-01-23",
"version": "2.1",
"total_sources": 16,
"description": "Primary references for logs/metrics/traces correlation, SLO/error budgets, and using observability as a QA signal."
},
"categories": {
"standards_and_core_docs": [
{
"name": "OpenTelemetry Documentation",
"url": "https://opentelemetry.io/docs/",
"description": "Vendor-neutral standard for traces, metrics, and logs.",
"add_as_web_search": true,
"optional": false
},
{
"name": "W3C Trace Context",
"url": "https://www.w3.org/TR/trace-context/",
"description": "Trace propagation standard (`traceparent`).",
"add_as_web_search": false,
"optional": false
}
],
"slo_and_error_budgets": [
{
"name": "Google SRE Book - Service Level Objectives",
"url": "https://sre.google/sre-book/service-level-objectives/",
"description": "SLOs and error budgets as reliability/quality gates.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Google SRE Workbook - Table of Contents",
"url": "https://sre.google/workbook/table-of-contents/",
"description": "Operational practices for implementing SRE patterns (runbooks, monitoring, incident response).",
"add_as_web_search": true,
"optional": false
}
],
"metrics_and_dashboards": [
{
"name": "Prometheus Documentation",
"url": "https://prometheus.io/docs/",
"description": "Metrics collection, PromQL, alerting rules.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Prometheus - Best Practices",
"url": "https://prometheus.io/docs/practices/",
"description": "Instrumentation and alerting best practices.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Grafana Documentation",
"url": "https://grafana.com/docs/grafana/latest/",
"description": "Dashboards, alerting, and visualization practices.",
"add_as_web_search": true,
"optional": false
}
],
"logging": [
{
"name": "The Twelve-Factor App - Logs",
"url": "https://12factor.net/logs",
"description": "Logs as event streams; baseline for cloud-native debugging.",
"add_as_web_search": false,
"optional": false
},
{
"name": "Grafana Loki",
"url": "https://grafana.com/docs/loki/latest/",
"description": "Log aggregation and querying patterns.",
"add_as_web_search": true,
"optional": false
}
],
"performance_and_testing": [
{
"name": "k6 Documentation",
"url": "https://k6.io/docs/",
"description": "Load testing scenarios and performance baselines.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Web Vitals",
"url": "https://web.dev/vitals/",
"description": "Frontend performance signals and budgets.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Lighthouse CI",
"url": "https://github.com/GoogleChrome/lighthouse-ci",
"description": "Automated performance testing in CI.",
"add_as_web_search": true,
"optional": false
},
{
"name": "Parca - Continuous Profiling",
"url": "https://www.parca.dev/docs/overview/",
"description": "Open-source eBPF-based continuous profiling for production systems.",
"add_as_web_search": true,
"optional": true
}
],
"ebpf_and_advanced": [
{
"name": "OpenTelemetry eBPF Instrumentation",
"url": "https://opentelemetry.io/docs/zero-code/",
"description": "Zero-code auto-instrumentation using eBPF (OpenTelemetry zero-code / eBPF instrumentation).",
"add_as_web_search": true,
"optional": true
},
{
"name": "eBPF Foundation",
"url": "https://ebpf.io/applications/",
"description": "eBPF observability tools landscape and applications.",
"add_as_web_search": true,
"optional": true
}
],
"optional_ai_automation": [
{
"name": "NIST AI Risk Management Framework",
"url": "https://www.nist.gov/itl/ai-risk-management-framework",
"description": "Optional governance baseline when adopting AI-based alert clustering or automated remediation.",
"add_as_web_search": true,
"optional": true
}
]
}
}
Alerting Strategies
Alert design, routing, and fatigue reduction for production systems. Build alerts that are actionable, not noisy.
Contents
- Alerting Philosophy
- Multi-Window Burn-Rate Alerts
- Alert Severity Levels
- Routing and Escalation
- Alert Fatigue Reduction
- Alert-on-SLO-Burn Approach
- Actionable Alert Templates
- On-Call Rotation Best Practices
- Alert Testing and Validation
- Alert Coverage Audit Checklist
- Related Resources
---
Alerting Philosophy
Symptom-Based, Not Cause-Based
Alert on what users experience, not on infrastructure internals.
| Approach | Example | Problem |
|---|---|---|
| Cause-based (avoid) | CPU > 80% | CPU can be high without user impact |
| Cause-based (avoid) | Disk > 90% | May not affect anything for days |
| Symptom-based (prefer) | Error rate > 1% | Users are seeing errors |
| Symptom-based (prefer) | P99 latency > 2s | Users are experiencing slowness |
| SLO-based (best) | Error budget burn rate > 6x | User experience is degrading at an unsustainable rate |
Core Principles
1. Every alert must be actionable -- if there is no action to take, it should not page someone 2. Alerts should have runbooks -- link to resolution steps in the alert itself 3. Prefer fewer, higher-quality alerts -- one good SLO alert replaces ten infrastructure alerts 4. Alert on rates, not absolutes -- "error rate > 5%" not "errors > 100" 5. Use appropriate urgency -- not everything is a page; most things are tickets
Alert Decision Tree
Is a user affected right now?
├── Yes → Is it urgent (revenue, safety, data)?
│ ├── Yes → PAGE (P1/P2)
│ └── No → TICKET (P3)
└── No → Will a user be affected soon?
├── Yes (< 24h) → TICKET (P3)
└── No → DASHBOARD only (P4)---
Multi-Window Burn-Rate Alerts
Multi-window alerts reduce false positives by requiring sustained error rates across two time windows.
How Multi-Window Works
Single-window alert (noisy):
"Error rate > threshold for 5 minutes" → fires on brief spikes
Multi-window alert (precise):
"Error rate > threshold for BOTH 1 hour AND 5 minutes"
→ Long window catches sustained issues
→ Short window confirms issue is current (not just historical)Prometheus Implementation
# prometheus-rules.yaml
groups:
- name: slo-burn-rate-alerts
rules:
# P1: Fast burn - 2% of 30-day budget in 1 hour
# Long window: 1h at 14.4x burn rate
# Short window: 5m at 14.4x burn rate
- alert: SLOBurnRateCritical
expr: |
(
(1 - rate(http_requests_total{status!~"5.."}[1h])
/ rate(http_requests_total[1h]))
> (14.4 * 0.001)
)
and
(
(1 - rate(http_requests_total{status!~"5.."}[5m])
/ rate(http_requests_total[5m]))
> (14.4 * 0.001)
)
for: 2m
labels:
severity: critical
team: "{{ $labels.team }}"
annotations:
summary: "Critical SLO burn rate for {{ $labels.service }}"
description: >
Service {{ $labels.service }} is consuming error budget at 14.4x
the sustainable rate. At this pace, the 30-day error budget will
be exhausted in ~50 hours.
runbook: "https://runbooks.example.com/slo-burn-critical"
dashboard: "https://grafana.example.com/d/slo-overview"
# P2: Medium burn - 5% of budget in 6 hours
- alert: SLOBurnRateHigh
expr: |
(
(1 - rate(http_requests_total{status!~"5.."}[6h])
/ rate(http_requests_total[6h]))
> (6 * 0.001)
)
and
(
(1 - rate(http_requests_total{status!~"5.."}[30m])
/ rate(http_requests_total[30m]))
> (6 * 0.001)
)
for: 15m
labels:
severity: warning
team: "{{ $labels.team }}"
annotations:
summary: "High SLO burn rate for {{ $labels.service }}"
runbook: "https://runbooks.example.com/slo-burn-high"
# P3: Slow burn - 10% of budget in 3 days
- alert: SLOBurnRateSlow
expr: |
(
(1 - rate(http_requests_total{status!~"5.."}[3d])
/ rate(http_requests_total[3d]))
> (1 * 0.001)
)
and
(
(1 - rate(http_requests_total{status!~"5.."}[6h])
/ rate(http_requests_total[6h]))
> (1 * 0.001)
)
for: 1h
labels:
severity: info
team: "{{ $labels.team }}"
annotations:
summary: "Slow SLO burn rate for {{ $labels.service }}"
runbook: "https://runbooks.example.com/slo-burn-slow"Burn Rate Reference Table
| Alert Level | Long Window | Short Window | Burn Rate | Budget Consumed | Time to Exhaustion |
|---|---|---|---|---|---|
| Critical (P1) | 1h | 5m | 14.4x | 2% in 1h | ~50 hours |
| High (P2) | 6h | 30m | 6x | 5% in 6h | ~5 days |
| Slow (P3) | 3d | 6h | 1x | 10% in 3d | ~30 days |
---
Alert Severity Levels
Severity Definitions
| Level | Name | Response Time | Notification | Examples |
|---|---|---|---|---|
| P1 | Critical | < 5 min | Page on-call immediately | Service down, data loss, security breach |
| P2 | High | < 30 min | Page on-call during business hours; Slack + ticket off-hours | Degraded performance, partial outage, SLO burn > 6x |
| P3 | Medium | < 4 hours | Slack notification + ticket | Slow burn, capacity warning, non-critical errors |
| P4 | Low | Next business day | Ticket only | Minor issues, cosmetic errors, optimization opportunities |
Severity Assignment Criteria
"""
Determine alert severity based on user impact and urgency.
"""
def determine_severity(
users_affected_pct: float,
is_revenue_impacting: bool,
is_data_loss_risk: bool,
is_security_incident: bool,
can_wait_hours: bool,
) -> str:
# P1: Immediate page
if is_security_incident:
return "P1"
if is_data_loss_risk:
return "P1"
if users_affected_pct > 50 and is_revenue_impacting:
return "P1"
# P2: Urgent but not emergency
if users_affected_pct > 10:
return "P2"
if is_revenue_impacting and not can_wait_hours:
return "P2"
# P3: Needs attention soon
if users_affected_pct > 1:
return "P3"
if not can_wait_hours:
return "P3"
# P4: Track and fix
return "P4"---
Routing and Escalation
PagerDuty Configuration
# pagerduty-service-config.yaml
services:
- name: order-service
escalation_policy:
- level: 1
targets:
- type: schedule
id: order-service-oncall
escalation_timeout_minutes: 15
- level: 2
targets:
- type: schedule
id: platform-team-oncall
escalation_timeout_minutes: 30
- level: 3
targets:
- type: user
id: engineering-manager
escalation_timeout_minutes: 60
alert_grouping:
type: intelligent
config:
time_window: 300 # 5 minutes
fields: ["service", "alert_name"]
auto_resolve_timeout: 14400 # 4 hours
acknowledgement_timeout: 1800 # 30 minutesRouting Rules
# alertmanager.yaml (Prometheus Alertmanager)
route:
receiver: default-slack
group_by: ["alertname", "service", "namespace"]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# P1 Critical: page immediately
- match:
severity: critical
receiver: pagerduty-critical
group_wait: 10s
repeat_interval: 5m
continue: true
# P1 also goes to Slack for visibility
- match:
severity: critical
receiver: slack-incidents
# P2 High: page during business hours, Slack always
- match:
severity: warning
receiver: pagerduty-warning
active_time_intervals:
- business_hours
continue: true
- match:
severity: warning
receiver: slack-alerts
# P3/P4: Slack and ticket only
- match:
severity: info
receiver: slack-alerts
receivers:
- name: pagerduty-critical
pagerduty_configs:
- routing_key: "<PAGERDUTY_CRITICAL_KEY>"
severity: critical
- name: pagerduty-warning
pagerduty_configs:
- routing_key: "<PAGERDUTY_WARNING_KEY>"
severity: warning
- name: slack-incidents
slack_configs:
- channel: "#incidents"
title: "{{ .GroupLabels.alertname }}"
text: "{{ range .Alerts }}{{ .Annotations.description }}{{ end }}"
- name: slack-alerts
slack_configs:
- channel: "#alerts"
title: "{{ .GroupLabels.alertname }}"---
Alert Fatigue Reduction
Alert fatigue is the number one killer of on-call effectiveness. When everything pages, nothing pages.
Fatigue Reduction Strategies
| Strategy | Description | Impact |
|---|---|---|
| Deduplication | Group identical alerts into one notification | High |
| Suppression | Suppress child alerts when parent fires | High |
| Grouping | Batch related alerts into single notification | Medium |
| Inhibition | Suppress lower-severity alerts when higher fires | Medium |
| Flap detection | Suppress alerts that toggle rapidly | Medium |
| Time-based routing | Non-critical alerts only during business hours | Medium |
| Alert review | Quarterly review: delete, tune, or merge | High |
Alertmanager Inhibition Rules
# Suppress service-level alerts when cluster is down
inhibit_rules:
- source_match:
alertname: ClusterDown
target_match_re:
alertname: ".+"
equal: ["namespace"]
- source_match:
severity: critical
target_match:
severity: warning
equal: ["alertname", "service"]Alert Audit Metrics
Track these metrics to detect and reduce fatigue:
"""
Alert fatigue metrics. Track weekly and trend over time.
"""
FATIGUE_METRICS = {
"total_alerts_fired": "Total alerts fired this week",
"unique_alerts_fired": "Unique alert names that fired",
"pages_per_oncall_shift": "Pages received per on-call shift",
"acknowledged_within_sla": "% alerts acknowledged within target",
"auto_resolved_pct": "% alerts that resolved without human action",
"false_positive_pct": "% alerts that required no action",
"mean_time_to_ack": "Average time from alert to acknowledgement",
"repeat_offenders": "Alert names that fired > 5x in a week",
}
# Healthy targets
TARGETS = {
"pages_per_oncall_shift": {"max": 2, "unit": "pages per 12h shift"},
"false_positive_pct": {"max": 5, "unit": "%"},
"auto_resolved_pct": {"max": 20, "unit": "%"},
"acknowledged_within_sla": {"min": 95, "unit": "%"},
}---
Alert-on-SLO-Burn Approach
Replace infrastructure-based alerts with SLO-based alerts for fewer, higher-quality notifications.
Before: Infrastructure Alerts (Noisy)
# 15+ alerts per service, most not actionable
alerts:
- CPU > 80%
- Memory > 85%
- Disk > 90%
- GC pause > 500ms
- Thread pool exhausted
- Connection pool > 80%
- Queue depth > 1000
- Pod restarts > 3
- Request count spike
- Error count > 50
- Latency P99 > 2s
- 5xx rate > 1%
- Health check failed
- SSL cert expiring
- DNS resolution slowAfter: SLO-Based Alerts (Focused)
# 3 alerts per service, all actionable
alerts:
- name: availability-slo-burn
description: "Error budget burning faster than sustainable"
windows: [1h/5m, 6h/30m, 3d/6h]
action: "Check runbook, likely code or dependency issue"
- name: latency-slo-burn
description: "Latency budget burning faster than sustainable"
windows: [1h/5m, 6h/30m, 3d/6h]
action: "Check runbook, likely capacity or query issue"
- name: error-budget-exhausted
description: "Monthly error budget depleted"
action: "Feature freeze, reliability work only"Migration Checklist: Infrastructure to SLO Alerts
- [ ] Define SLIs for each service (availability, latency)
- [ ] Set SLO targets based on historical data
- [ ] Implement multi-window burn-rate alerts
- [ ] Run SLO alerts alongside existing alerts for 2 weeks
- [ ] Compare: which SLO alerts would have caught real incidents?
- [ ] Disable infrastructure alerts caught by SLO alerts
- [ ] Keep infrastructure alerts only for things SLOs cannot detect (disk full, cert expiry)
- [ ] Review monthly: tune thresholds, remove noise
---
Actionable Alert Templates
Every alert should contain enough context to start diagnosis without looking elsewhere.
Template Structure
# Required fields for every alert
alert_template:
name: "Descriptive name (not abbreviations)"
severity: "P1|P2|P3|P4"
summary: "One sentence: what is happening"
description: |
What: Error rate for {{ service }} exceeded {{ threshold }}
Impact: {{ affected_users }}% of users seeing errors
Since: {{ started_at }}
Current value: {{ current_value }}
runbook_url: "https://runbooks.example.com/{{ alert_name }}"
dashboard_url: "https://grafana.example.com/d/{{ service }}-overview"
labels:
service: "{{ service }}"
team: "{{ owning_team }}"
environment: "{{ env }}"Example: Complete Alert Definition
groups:
- name: order-service-slo
rules:
- alert: OrderServiceAvailabilitySLOBurn
expr: |
(
1 - (
sum(rate(http_requests_total{service="order-service",status!~"5.."}[1h]))
/ sum(rate(http_requests_total{service="order-service"}[1h]))
)
) > (14.4 * 0.001)
for: 2m
labels:
severity: critical
service: order-service
team: commerce
slo: availability
annotations:
summary: "Order Service availability SLO burn rate critical"
description: |
The order service error budget is burning at 14.4x the
sustainable rate. At this pace, the monthly budget will be
exhausted in approximately 50 hours.
Current error rate: {{ $value | humanizePercentage }}
SLO target: 99.9%
Likely causes:
- Recent deployment (check: kubectl rollout history)
- Downstream dependency failure (check: dependency dashboard)
- Database connection issues (check: connection pool metrics)
runbook_url: "https://runbooks.example.com/order-service/high-error-rate"
dashboard_url: "https://grafana.example.com/d/order-service-slo"---
On-Call Rotation Best Practices
| Practice | Recommendation | Rationale |
|---|---|---|
| Rotation length | 1 week | Short enough to stay engaged, long enough for context |
| Handoff meeting | 30 min overlap | Transfer active issues, recent changes |
| Shadow shifts | 2 shifts before primary | New on-call shadows experienced engineer |
| Max pages per shift | 2 per 12h shift | More than this indicates alert quality issues |
| Compensation | Extra PTO or pay | On-call has real personal cost |
| Post-incident review | Within 48h | Capture learnings while fresh |
| Quarterly review | Audit alert volume and quality | Continuous improvement |
Handoff Template
## On-Call Handoff: [Date]
### Active Issues
- [ ] Order service elevated latency (P3, tracking in JIRA-1234)
- [ ] Payment provider intermittent timeouts (monitoring, no action needed)
### Recent Changes
- Deployed order-service v2.4.1 (Tuesday)
- Database migration ran (Wednesday, all green)
- New alert added: payment-slo-burn (Thursday)
### Known Risks
- Black Friday traffic expected 3x normal (Saturday)
- Payment provider maintenance window (Sunday 2-4am UTC)
### Runbook Updates
- Updated: order-service/high-error-rate (new rollback command)
- Added: payment-service/provider-timeout---
Alert Testing and Validation
Testing Alerts Before Production
#!/bin/bash
# test-alert-rules.sh: Validate alert rules without deploying
# Syntax check Prometheus alert rules
promtool check rules alerts/*.yaml
# Unit test alert rules against recorded metrics
promtool test rules tests/alert-tests.yaml
# Validate Alertmanager config
amtool check-config alertmanager.yaml
# Test routing decisions
amtool config routes test \
--config.file=alertmanager.yaml \
--verify.receivers=pagerduty-critical \
alertname=SLOBurnRateCritical severity=critical service=order-serviceAlert Unit Tests
# tests/alert-tests.yaml
rule_files:
- alerts/slo-burn-rate.yaml
evaluation_interval: 1m
tests:
- interval: 1m
input_series:
- series: 'http_requests_total{service="order-service",status="200"}'
values: "100+100x60" # 100 requests per minute, all successful
- series: 'http_requests_total{service="order-service",status="500"}'
values: "0+0x60" # No errors
alert_rule_test:
- eval_time: 60m
alertname: SLOBurnRateCritical
exp_alerts: [] # Should NOT fire when everything is healthy
- interval: 1m
input_series:
- series: 'http_requests_total{service="order-service",status="200"}'
values: "90+90x60" # 90% success rate
- series: 'http_requests_total{service="order-service",status="500"}'
values: "10+10x60" # 10% error rate
alert_rule_test:
- eval_time: 10m
alertname: SLOBurnRateCritical
exp_alerts:
- exp_labels:
severity: critical
service: order-service---
Alert Coverage Audit Checklist
Per-Service Alert Audit
- [ ] SLO alerts exist for availability and latency
- [ ] Multi-window burn rates configured (fast, medium, slow)
- [ ] Runbook linked in every alert annotation
- [ ] Dashboard linked in every alert annotation
- [ ] Severity correctly assigned (P1-P4)
- [ ] Routing verified (correct team, correct channel)
- [ ] Escalation policy defined (L1, L2, L3)
- [ ] Alert tested with unit tests or historical data replay
- [ ] False positive rate < 5% over last 30 days
- [ ] Alert acknowledged within SLA over last 30 days
Organizational Alert Health
- [ ] Pages per on-call shift < 2 average
- [ ] No repeat offender alerts (same alert > 5x/week without fix)
- [ ] All alerts have owners (team label)
- [ ] Quarterly alert review completed
- [ ] Alert documentation up to date
- [ ] On-call handoff process documented and followed
- [ ] Shadow rotation in place for new team members
---
Related Resources
- SLO Design Guide - Defining SLOs and error budgets
- Dashboard Design Patterns - Visualization for observability
- Core Observability Patterns - Metrics, logs, traces fundamentals
- Log Aggregation Patterns - Structured logging pipelines
- Anti-Patterns and Best Practices - Common observability mistakes
- Runbook Testing - Testing the runbooks alerts link to
- SKILL.md - Parent skill overview
Anti-Patterns & Best Practices
Common observability mistakes and how to avoid them, based on production experience from thousands of teams.
Contents
- Critical Anti-Patterns to Avoid
- Best Practices Summary
- Decision Matrix: When to Use What
- ROI Validation
Critical Anti-Patterns to Avoid
1. Logging Everything (Log Bloat)
Anti-Pattern:
// Logging every function call
function processOrder(order) {
logger.info('processOrder called', { order });
logger.info('Validating order');
const isValid = validateOrder(order);
logger.info('Validation result', { isValid });
logger.info('Saving to database');
const saved = db.save(order);
logger.info('Saved to database', { saved });
logger.info('Sending email');
sendEmail(order);
logger.info('Email sent');
logger.info('processOrder completed');
}Why It's Bad:
- High-cardinality data bloats logs (every order ID, user ID)
- 90% of logs are noise, 10% are signal
- Expensive log storage costs ($1000s/month for 100GB/day)
- Slow log search (searching through terabytes)
Best Practice:
// Log only important events and errors
function processOrder(order) {
const span = tracer.startSpan('process-order');
span.setAttribute('order.id', order.id);
span.setAttribute('user.id', order.userId);
try {
validateOrder(order);
db.save(order);
sendEmail(order);
logger.info('Order processed successfully', { order_id: order.id });
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
logger.error('Order processing failed', { order_id: order.id, error });
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
}Rule of Thumb:
- Logs: Important business events (order created, payment failed)
- Traces: Execution flow and timing (function entry/exit, database calls)
- Metrics: Aggregated data (request count, latency percentiles)
---
2. No Sampling (100% Trace Collection)
Anti-Pattern:
// Collecting 100% of traces in production
const provider = new TracerProvider({
sampler: new AlwaysOnSampler(), // Samples every single trace
});Why It's Bad:
- 10k RPS = 864M traces/day = $10k-50k/month storage
- High write load on trace backend (Jaeger, Tempo)
- Most traces are identical (successful requests)
Best Practice:
// Adaptive sampling: 100% errors, 10% success
const provider = new TracerProvider({
sampler: new ParentBasedSampler({
root: new TraceIdRatioBasedSampler(0.1), // 10% of root spans
}),
});
// Or custom sampler
class AdaptiveSampler {
shouldSample(context, traceId, spanName, spanKind, attributes, links) {
// Always sample errors
if (attributes['http.status_code'] >= 500) {
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
// Always sample slow requests
if (attributes['http.duration_ms'] > 1000) {
return { decision: SamplingDecision.RECORD_AND_SAMPLED };
}
// Sample 1% of normal requests
return Math.random() < 0.01
? { decision: SamplingDecision.RECORD_AND_SAMPLED }
: { decision: SamplingDecision.NOT_RECORD };
}
}Sampling Strategy by Traffic Volume:
| RPS | Recommended Sampling | Traces/Day | Est. Cost/Month |
|---|---|---|---|
| 100 | 100% | 8.6M | $50-100 |
| 1k | 10% | 8.6M | $50-100 |
| 10k | 1% | 8.6M | $50-100 |
| 100k | 0.1% | 8.6M | $50-100 |
Target: ~10M traces/day for cost-effective observability.
---
3. Alert Fatigue (Too Many Noisy Alerts)
Anti-Pattern:
# Alerting on every metric spike
alerts:
- alert: HighCPU
expr: cpu_usage > 50%
for: 1m
- alert: HighMemory
expr: memory_usage > 50%
for: 1m
- alert: HighLatency
expr: http_latency_p99 > 100ms
for: 1m
- alert: AnyError
expr: error_count > 0
for: 1mWhy It's Bad:
- 100s of alerts/day -> engineers ignore alerts
- False positives (CPU spike during deployment)
- No context (is this actually impacting users?)
Best Practice (SLO-Based Alerting):
# Alert on SLO burn rate
alerts:
# Fast burn: 2% budget consumed in 1 hour = P0 incident
- alert: ErrorBudgetFastBurn
expr: |
(1 - slo:availability:ratio_rate1h) > (14.4 * (1 - 0.999))
labels:
severity: critical
annotations:
summary: "Fast burn rate - 2% error budget consumed in 1 hour"
# Slow burn: 5% budget consumed in 6 hours = P1 warning
- alert: ErrorBudgetSlowBurn
expr: |
(1 - slo:availability:ratio_rate6h) > (2.4 * (1 - 0.999))
labels:
severity: warning
annotations:
summary: "Slow burn rate - 5% error budget consumed in 6 hours"Alerting Philosophy:
- Alert on user impact, not infrastructure metrics
- Use multi-window burn rate (fast: 1h, slow: 6h)
- Target 5-10 alerts/day (not 100s)
- Every alert should be actionable (runbook required)
---
4. Ignoring Tail Latency (Only Monitoring Averages)
Anti-Pattern:
# Only tracking average latency
avg(http_request_duration_seconds)Why It's Bad:
- Average latency can be 100ms while P99 is 10s
- 1% of users experience terrible performance
- Tail latency often reveals systemic issues
Example:
100 requests:
- 99 requests: 50ms (average: 50ms)
- 1 request: 10,000ms (P99: 10,000ms)
Average latency: 149ms [OK] "Looks good"
P99 latency: 10,000ms [FAIL] "1% of users wait 10 seconds"Best Practice:
# Track percentiles (P50, P95, P99, P999)
histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m])) # P50
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) # P95
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) # P99
histogram_quantile(0.999, rate(http_request_duration_seconds_bucket[5m])) # P999SLO Definition:
slos:
- name: api-latency-p99
sli: http_request_duration_p99
target: 500ms # 99% of requests < 500ms
window: 30dWhy P99 Matters:
- 10k RPS -> 100 slow requests/second
- 100 slow requests/sec -> 360k unhappy users/hour
- Tail latency reveals database hotspots, cache misses, GC pauses
---
5. No Error Budgets (Move Too Slow or Too Fast)
Anti-Pattern:
Team A: "We can't ship this feature, it might break production"
-> 100% reliability target, 0 features shipped
Team B: "Ship fast, break things"
-> 95% reliability, constant outagesWhy It's Bad:
- Without error budgets, teams either move too slow (fear) or too fast (chaos)
- No quantifiable trade-off between velocity and reliability
- Political debates instead of data-driven decisions
Best Practice (Error Budget Policy):
SLO: 99.9% availability over 30 days
Error Budget: 43.2 minutes downtime/month
| Error Budget Remaining | Action |
|------------------------|--------|
| > 50% | Full velocity (all features, experiments, rewrites) |
| 25-50% | Cautious (critical features only, no experiments) |
| 10-25% | Feature freeze (reliability work only) |
| < 10% | Incident mode (stop all releases, rollback) |Example:
Month 1:
- Shipped 10 features
- Had 2 outages (30 minutes total)
- Error budget remaining: 30% (13 minutes left)
- Action: Feature freeze, focus on reliability
Month 2:
- Shipped 0 features (freeze mode)
- Added retries, circuit breakers, better monitoring
- Had 0 outages
- Error budget remaining: 100% (43.2 minutes available)
- Action: Resume full velocityBenefits:
- [OK] Quantifiable reliability vs velocity trade-off
- [OK] Data-driven decisions (not political)
- [OK] Incentivizes reliability work (replenish error budget)
- [OK] Prevents over-engineering (don't target 100% uptime)
---
6. Metrics Without Context (Dashboard Mysteries)
Anti-Pattern:
Grafana Dashboard:
- CPU: 80% (is this normal? abnormal?)
- Latency: 200ms (is this good? bad?)
- Error Rate: 0.5% (should I be worried?)Why It's Bad:
- No baseline (is 80% CPU normal during peak hours?)
- No annotations (was there a deployment? traffic spike?)
- No SLO context (is 0.5% error rate within budget?)
Best Practice:
Grafana Dashboard (with context):
- CPU: 80% (normal during peak hours: 70-85%)
[Annotation: Deployment at 10:30 AM]
- Latency P99: 450ms (SLO target: <500ms, 90% of budget used)
[Annotation: Traffic spike from marketing campaign]
- Error Rate: 0.5% (SLO target: <0.1%, OVER BUDGET [FAIL])
[Alert: Error budget exhausted, feature freeze active]Add Context with:
- Baselines: Show expected range (min/max/avg)
- Annotations: Mark deployments, incidents, campaigns
- SLO indicators: Show how close to SLO target
- Related metrics: Correlated graphs (latency + error rate + traffic)
---
7. No Cost Tracking (Observability Costs 20% of Infrastructure)
Anti-Pattern:
"We need observability, enable everything!"
-> $50k/month infrastructure
-> $10k/month observability (20% overhead)
-> Team doesn't realize costWhy It's Bad:
- Observability can cost 10-20% of infrastructure
- Log storage grows unbounded (1TB/day = $1000s/month)
- Trace storage grows exponentially (10M traces/day = $5k/month)
Cost Breakdown (Typical 100 RPS Service):
| Component | Volume | Cost/Month |
|---|---|---|
| Log storage (7-day retention) | 100GB/day | $500 |
| Metrics storage (Prometheus) | 1M series | $200 |
| Trace storage (1% sampling) | 10M traces/day | $1000 |
| APM (Datadog, New Relic) | 10 hosts | $1500 |
| Total | $3200/month |
Best Practice:
// Log sampling (only sample 10% of successful requests)
logger.info({ order_id: '123', sample_rate: 0.1 }, 'Order processed');
// Trace sampling (1% of successful requests, 100% of errors)
const provider = new TracerProvider({
sampler: new AdaptiveSampler(), // Custom sampler
});
// Metric cardinality limits (avoid user_id in metrics)
// Bad: metrics.counter('orders', { user_id: '123' }); // 1M unique users = 1M series
// Good: metrics.counter('orders', { status: 'success' }); // 2 series (success, failure)Cost Optimization Strategies: 1. Log retention: 7 days hot, 30 days cold, 90 days archive 2. Trace sampling: 1% for 10k RPS, 0.1% for 100k RPS 3. Metric cardinality: <1000 unique label combinations per metric 4. APM: Use open source (Jaeger, Grafana) instead of commercial ($200/month vs $1500/month)
---
8. Point-in-Time Profiling (Missing Intermittent Issues)
Anti-Pattern:
# Manual profiling when users report slowness
node --prof app.js
# Run for 5 minutes, analyze profile
node --prof-process isolate-*.logWhy It's Bad:
- Intermittent issues only happen at 3 AM on Tuesdays
- Performance issues correlate with specific user actions
- Manual profiling misses root cause
Best Practice (Continuous Profiling):
// Automatic heap snapshots every hour
const v8 = require('v8');
const fs = require('fs');
setInterval(() => {
const filename = `heap-${Date.now()}.heapsnapshot`;
v8.writeHeapSnapshot(filename);
// Upload to S3, analyze for memory leaks
uploadToS3(filename);
analyzeForLeaks(filename);
}, 60 * 60 * 1000); // Every hour
// Or use continuous profiling tools
// - Pyroscope (open source)
// - Google Cloud Profiler
// - Datadog Continuous ProfilerBenefits:
- [OK] Catch intermittent issues (memory leaks, GC pauses)
- [OK] Historical profiling data (compare before/after deployment)
- [OK] Correlate performance with traffic patterns
- [OK] Proactive optimization (before users complain)
---
Best Practices Summary
Do's:
- [OK] Log important business events, use traces for execution flow
- [OK] Sample traces intelligently (100% errors, 1-10% success)
- [OK] Alert on SLO burn rate, not infrastructure metrics
- [OK] Track tail latency (P99, P999), not just averages
- [OK] Use error budgets to balance velocity vs reliability
- [OK] Add context to dashboards (baselines, annotations, SLOs)
- [OK] Track observability costs, optimize aggressively
- [OK] Continuous profiling for intermittent issues
Don'ts:
- [FAIL] Log everything (bloats logs, high cardinality)
- [FAIL] Collect 100% of traces (expensive, unnecessary)
- [FAIL] Alert on every metric spike (alert fatigue)
- [FAIL] Only monitor averages (tail latency matters)
- [FAIL] Target 100% reliability (no error budget = no velocity)
- [FAIL] Dashboards without context (mysteries)
- [FAIL] Ignore observability costs (20% overhead)
- [FAIL] Point-in-time profiling (misses intermittent issues)
---
Decision Matrix: When to Use What
| Scenario | Use This | Not This |
|---|---|---|
| Track order created | Log (INFO) | Trace every step |
| Debug slow request | Distributed trace | Logs in 10 services |
| Alert on user impact | SLO burn rate | CPU >80% |
| Measure performance | P99 latency | Average latency |
| Balance velocity/reliability | Error budgets | "Move fast" or "Never break" |
| Understand dashboard spike | Annotations + SLO context | Raw metrics |
| Reduce observability costs | Sampling + retention policies | Collect everything |
| Find memory leaks | Continuous profiling | Manual profiling |
---
ROI Validation
Observability Investment vs Returns:
Investment (100 RPS service):
- Tools: $3k/month (logs, metrics, traces, APM)
- Engineering: 2 engineers * $150k/year = $25k/month
- Total: ~$30k/month
Returns:
- MTTR: 2 hours -> 10 minutes (12x faster incident resolution)
- MTTD: 1 hour -> 5 minutes (12x faster detection)
- Incident cost: $10k/hour * 2 hours = $20k -> $10k/hour * 10 min = $1.7k
- Savings: $18k/incident
- If 3 incidents/month -> $54k/month savings
- ROI: $54k savings - $30k investment = $24k/month profitWhen NOT to Invest in Advanced Observability:
- Early-stage startup (<10 RPS, <5 engineers)
- Prototype/MVP (focus on product-market fit first)
- Internal tools (low SLO requirements)
When to Invest:
- Production systems (>100 RPS, >10 engineers)
- Mission-critical services (financial, healthcare)
- High incident costs ($10k-100k/hour downtime)
---
Golden Rule: Observability should cost 10-20% of infrastructure, not 50%. Optimize aggressively (sampling, retention, cardinality) while maintaining debugging capabilities.