Complete Guide to Monitoring and Alerting
Build a production monitoring stack. Covers Prometheus, Grafana, AlertManager, metrics instrumentation, alert rules, runbooks, SLI/SLO/SLA, distributed tracing with Jaeger, log aggregation, and on-call best practices with practical configuration examples.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Introduction
Monitoring tells you what is happening in your system. Alerting tells you when something needs attention. Together they are the foundation of observability. Here is a hands-on guide to Prometheus, Grafana, AlertManager, metrics instrumentation, alert rules, runbooks, SLI/SLO/SLA, distributed tracing, log aggregation, and on-call practices.
Prometheus Setup
Installation
# prometheus-values.yaml (Helm)
server:
persistentVolume:
enabled: true
size: 50Gi
storageClass: gp3
retention: 30d
retentionSize: 45GB
resources:
limits:
cpu: 2
memory: 4Gi
requests:
cpu: 1
memory: 2Gi
alertmanager:
enabled: true
persistentVolume:
enabled: true
size: 5Gi
nodeExporter:
enabled: true
kubeStateMetrics:
enabled: true
extraScrapeConfigs: |
- job_name: 'api-service'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
target_label: __address__
regex: (.+)
replacement: $1
# Install Prometheus with Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/prometheus \
-n monitoring --create-namespace \
-f prometheus-values.yaml
Metrics Instrumentation
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
from fastapi import FastAPI, Request, Response
import time
app = FastAPI()
# Metrics definitions
REQUEST_COUNT = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'http_request_duration_seconds',
'HTTP request latency',
['method', 'endpoint'],
buckets=[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)
ACTIVE_CONNECTIONS = Gauge(
'active_connections',
'Active connections'
)
DB_POOL_SIZE = Gauge(
'db_pool_size',
'Database connection pool size',
['pool_name']
)
DB_POOL_USED = Gauge(
'db_pool_used',
'Database connections in use',
['pool_name']
)
BUSINESS_METRIC = Counter(
'api_requests_total',
'Total API requests by plan',
['plan', 'endpoint']
)
# Middleware for automatic metrics
@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
start = time.time()
ACTIVE_CONNECTIONS.inc()
try:
response = await call_next(request)
duration = time.time() - start
endpoint = request.url.path
method = request.method
status = response.status_code
REQUEST_COUNT.labels(method=method, endpoint=endpoint, status=status).inc()
REQUEST_LATENCY.labels(method=method, endpoint=endpoint).observe(duration)
return response
finally:
ACTIVE_CONNECTIONS.dec()
# Metrics endpoint
@app.get("/metrics")
async def metrics():
return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)
# Business metrics in endpoints
@app.get("/api/users")
async def list_users(request: Request):
plan = request.headers.get("x-plan", "free")
BUSINESS_METRIC.labels(plan=plan, endpoint="/api/users").inc()
# ... handler logic
return {"users": []}
# Update gauges periodically
import asyncio
async def update_db_pool_metrics():
while True:
for pool_name, pool in db.pools.items():
DB_POOL_SIZE.labels(pool_name=pool_name).set(pool.size())
DB_POOL_USED.labels(pool_name=pool_name).set(pool.used())
await asyncio.sleep(15)
@app.on_event("startup")
async def start_metrics_updater():
asyncio.create_task(update_db_pool_metrics())
Grafana Dashboards
Dashboard as Code
{
"dashboard": {
"title": "API Service Overview",
"tags": ["api", "production"],
"timezone": "browser",
"panels": [
{
"title": "Request Rate (req/s)",
"type": "graph",
"datasource": "Prometheus",
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (endpoint)",
"legendFormat": "{{endpoint}}"
}
]
},
{
"title": "P99 Latency",
"type": "graph",
"datasource": "Prometheus",
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
"targets": [
{
"expr": "histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, endpoint))",
"legendFormat": "p99 {{endpoint}}"
}
]
},
{
"title": "Error Rate (%)",
"type": "stat",
"datasource": "Prometheus",
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 8},
"targets": [
{
"expr": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m])) * 100"
}
],
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{"color": "green", "value": null},
{"color": "yellow", "value": 1},
{"color": "red", "value": 5}
]
}
}
}
},
{
"title": "Active Connections",
"type": "stat",
"datasource": "Prometheus",
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 8},
"targets": [
{"expr": "active_connections"}
]
}
],
"templating": {
"list": [
{
"name": "datasource",
"type": "datasource",
"query": "prometheus"
}
]
},
"time": {"from": "now-1h", "to": "now"},
"refresh": "30s"
}
}
Grafana in Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:11.0.0
ports:
- containerPort: 3000
env:
- name: GF_SECURITY_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: grafana-secrets
key: admin-password
- name: GF_INSTALL_PLUGINS
value: "grafana-piechart-panel"
volumeMounts:
- name: grafana-storage
mountPath: /var/lib/grafana
- name: dashboards
mountPath: /var/lib/grafana/dashboards
- name: datasource
mountPath: /etc/grafana/provisioning/datasources
volumes:
- name: grafana-storage
persistentVolumeClaim:
claimName: grafana-pvc
- name: dashboards
configMap:
name: grafana-dashboards
- name: datasource
configMap:
name: grafana-datasource
AlertManager
Alert Rules
# alerting-rules.yaml
groups:
- name: api-service
interval: 30s
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (endpoint)
/
sum(rate(http_requests_total[5m])) by (endpoint)
> 0.05
for: 5m
labels:
severity: critical
team: platform
annotations:
summary: "High error rate on {{ $labels.endpoint }}"
description: "Error rate is {{ $value | humanizePercentage }} for the last 5 minutes"
runbook: "https://runbooks.stackpractices.com/high-error-rate"
- alert: HighLatency
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, endpoint))
> 2
for: 10m
labels:
severity: warning
team: platform
annotations:
summary: "High P99 latency on {{ $labels.endpoint }}"
description: "P99 latency is {{ $value }}s for the last 10 minutes"
runbook: "https://runbooks.stackpractices.com/high-latency"
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[5m]) > 0
for: 5m
labels:
severity: critical
team: platform
annotations:
summary: "Pod {{ $labels.pod }} is crash looping"
description: "Container {{ $labels.container }} in pod {{ $labels.pod }} has restarted {{ $value }} times in the last 5 minutes"
runbook: "https://runbooks.stackpractices.com/crash-loop"
- alert: DiskSpaceLow
expr: |
(node_filesystem_avail_bytes{mountpoint="/"}
/
node_filesystem_size_bytes{mountpoint="/"}) * 100 < 10
for: 5m
labels:
severity: warning
team: platform
annotations:
summary: "Low disk space on {{ $labels.instance }}"
description: "Only {{ $value }}% disk space remaining on {{ $labels.instance }}"
runbook: "https://runbooks.stackpractices.com/low-disk-space"
- alert: DatabaseConnectionsExhausted
expr: |
db_pool_used / db_pool_size > 0.8
for: 5m
labels:
severity: warning
team: platform
annotations:
summary: "Database connection pool nearly exhausted"
description: "Pool {{ $labels.pool_name }} is at {{ $value | humanizePercentage }} capacity"
runbook: "https://runbooks.stackpractices.com/db-pool-exhausted"
AlertManager Configuration
# alertmanager.yaml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'cluster', 'severity']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default'
routes:
- matchers:
- severity = "critical"
receiver: 'critical-pagerduty'
group_wait: 10s
repeat_interval: 1h
- matchers:
- severity = "warning"
receiver: 'warning-slack'
repeat_interval: 4h
- matchers:
- team = "frontend"
receiver: 'frontend-slack'
receivers:
- name: 'default'
slack_configs:
- api_url: 'https://hooks.slack.com/services/xxx'
channel: '#alerts'
send_resolved: true
- name: 'critical-pagerduty'
pagerduty_configs:
- routing_key: 'your-pagerduty-routing-key'
severity: critical
send_resolved: true
- name: 'warning-slack'
slack_configs:
- api_url: 'https://hooks.slack.com/services/xxx'
channel: '#alerts-warnings'
send_resolved: true
- name: 'frontend-slack'
slack_configs:
- api_url: 'https://hooks.slack.com/services/xxx'
channel: '#frontend-alerts'
inhibit_rules:
- source_matchers:
- severity = "critical"
target_matchers:
- severity = "warning"
equal: ['alertname', 'cluster']
SLI, SLO, SLA
# SLI/SLO definitions as Prometheus rules
groups:
- name: slo-tracking
interval: 30s
rules:
# SLI: Request success rate
- record: job:slo_availability:ratio_rate5m
expr: |
sum(rate(http_requests_total{status!~"5.."}[5m])) by (job)
/
sum(rate(http_requests_total[5m])) by (job)
- record: job:slo_availability:ratio_rate1h
expr: |
sum(rate(http_requests_total{status!~"5.."}[1h])) by (job)
/
sum(rate(http_requests_total[1h])) by (job)
# SLI: Latency (P99 < 500ms)
- record: job:slo_latency:ratio_rate5m
expr: |
sum(rate(http_request_duration_seconds_bucket{le="0.5"}[5m])) by (job)
/
sum(rate(http_request_duration_seconds_count[5m])) by (job)
# Error budget burn rate
- record: job:error_budget_burn:ratio_rate1h
expr: |
(1 - job:slo_availability:ratio_rate1h) / (1 - 0.999)
# Alert: Error budget burning too fast
- alert: ErrorBudgetBurn
expr: job:error_budget_burn:ratio_rate1h > 14.4
for: 5m
labels:
severity: critical
annotations:
summary: "Error budget burning too fast for {{ $labels.job }}"
description: "Burning 1h of error budget every 5 minutes. SLO is at risk."
SLI/SLO/SLA Definitions:
SLI (Service Level Indicator):
- A measured metric: request success rate, latency P99, uptime
- Example: 99.9% of requests succeed (status < 500)
SLO (Service Level Objective):
- Target for your SLI over a time window
- Example: 99.9% availability over 30 days
- Error budget: 0.1% of 30 days = 43.2 minutes of allowed downtime
SLA (Service Level Agreement):
- Contract with customers, includes consequences for missing SLO
- Example: 99.5% uptime or refund 10% of monthly fee
Error Budget:
- The allowed amount of unreliability
- 99.9% SLO = 43.2 min downtime per 30 days
- If budget is spent, freeze new features and focus on reliability
- If budget is healthy, take risks (deploy more, experiment)
Distributed Tracing
# OpenTelemetry tracing setup
from opentelemetry import trace
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
def setup_tracing(app, service_name: str, jaeger_host: str = "jaeger"):
provider = TracerProvider()
jaeger_exporter = JaegerExporter(
agent_host_name=jaeger_host,
agent_port=6831,
)
provider.add_span_processor(
BatchSpanProcessor(jaeger_exporter)
)
trace.set_tracer_provider(provider)
# Auto-instrument FastAPI
FastAPIInstrumentor.instrument_app(app)
# Auto-instrument SQLAlchemy
SQLAlchemyInstrumentor().instrument(engine=db.engine)
# Auto-instrument HTTPX
HTTPXClientInstrumentor().instrument()
# Manual tracing
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
with tracer.start_as_current_span("get_user") as span:
span.set_attribute("user.id", user_id)
with tracer.start_as_current_span("db_query"):
user = db.query(User).filter(User.id == user_id).first()
if not user:
span.set_attribute("user.found", False)
raise HTTPException(404, "User not found")
span.set_attribute("user.found", True)
return {"id": user.id, "name": user.username}
Log Aggregation
# Loki + Promtail for log aggregation
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: promtail
namespace: monitoring
spec:
selector:
matchLabels:
app: promtail
template:
metadata:
labels:
app: promtail
spec:
containers:
- name: promtail
image: grafana/promtail:3.0.0
args:
- -config.file=/etc/promtail/promtail.yaml
volumeMounts:
- name: promtail-config
mountPath: /etc/promtail
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: promtail-config
configMap:
name: promtail-config
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
# promtail-config.yaml
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_node_name]
target_label: node
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
pipeline_stages:
- json:
expressions:
level: level
message: message
request_id: request_id
- labels:
level:
request_id:
Runbooks
# Runbook: High Error Rate
## Alert: HighErrorRate
- **Severity**: Critical
- **Condition**: Error rate > 5% for 5 minutes
- **Impact**: Users seeing 500 errors
## Quick Triage (5 minutes)
1. Check Grafana dashboard: "API Service Overview"
2. Identify which endpoints have high error rates
3. Check recent deployments: `kubectl rollout history deployment/api -n production`
4. Check pod logs: `kubectl logs -l app=api -n production --tail=100`
## Resolution Steps
### If caused by recent deployment
1. Rollback: `kubectl rollout undo deployment/api -n production`
2. Verify error rate drops: check Grafana
3. Create incident report
### If caused by database issues
1. Check database connections: `kubectl exec -it <db-pod> -- psql -c "SELECT count(*) FROM pg_stat_activity"`
2. Check for long queries: `SELECT pid, now() - query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' ORDER BY duration DESC LIMIT 10`
3. Kill long queries if needed: `SELECT pg_terminate_backend(pid)`
### If caused by external API failure
1. Check external API status page
2. Enable circuit breaker fallback
3. Increase timeout if temporary
## Post-Incident
1. Create postmortem within 48 hours
2. Add alert for earlier detection if possible
3. Update this runbook with new findings
FAQ
What metrics should I monitor?
Monitor four golden signals: latency (request duration histograms), traffic (request rate), errors (error rate by status code), and saturation (CPU, memory, disk, connection pool usage). Add business metrics specific to your application (active users, API calls by plan, revenue). Use the USE method for resources (Utilization, Saturation, Errors) and the RED method for services (Rate, Errors, Duration).
How do I set SLOs?
Start with user-facing metrics. Define what “good” means from the user’s perspective — e.g., “request completes in under 500ms with status < 500”. Set SLO based on historical performance, not aspiration. A 99.9% SLO means 43.2 minutes of allowed downtime per 30 days. Track error budget burn rate and alert when burning too fast. Review SLOs quarterly and adjust based on user feedback and business needs.
What is the difference between metrics, logs, and traces?
Metrics are aggregated, numeric time-series data (request count, CPU usage). They are cheap to store and good for alerting. Logs are discrete events with context (error message, request details). They are expensive to store but essential for debugging. Traces follow a single request across service boundaries. They show causality and timing between services. Use all three: metrics for alerting, logs for debugging, traces for understanding distributed behavior.
How do I prevent alert fatigue?
Set alert thresholds based on user impact, not infrastructure metrics. Alert on symptoms (high error rate, high latency) not causes (CPU > 80%). Use severity levels: critical pages on-call, warning sends Slack. Suppress alerts during known incidents with inhibit rules. Route alerts to the right team. Review alert noise monthly and tune thresholds. Every alert should have a runbook. If an alert fires and no action is needed, delete it.
Should I use PagerDuty or Slack for alerts?
Use PagerDuty (or Opsgenie) for critical alerts that require immediate human action — it ensures someone is paged and acknowledges. Use Slack for warning-level alerts and informational notifications. Never send critical alerts only to Slack — people may not see them for hours. AlertManager can route to both: PagerDuty for critical, Slack for warnings. Configure escalation policies in PagerDuty (primary, secondary, manager).
How do I instrument code for observability?
Use standard libraries: Prometheus client for metrics, OpenTelemetry SDK for traces, structured JSON logging. Instrument at framework level (middleware) so every request gets metrics automatically. Add business-specific metrics in handlers. Use trace spans for database queries, external API calls, and expensive computations. Set span attributes with relevant context (user ID, request ID, endpoint). Log structured events with correlation IDs that link to traces.
See Also
Related Resources
Complete Guide to Kubernetes Networking
Master Kubernetes networking. Covers Services, Ingress, NetworkPolicies, CNI plugins, DNS, service mesh, load balancing, external traffic, mTLS, and troubleshooting with practical YAML manifests and configuration examples.
GuideComplete Guide to GitOps in Production
Implement GitOps with ArgoCD and Flux. Covers declarative infrastructure, drift reconciliation, automated rollbacks, progressive delivery, multi-cluster management, secret management with SOPS, and CI/CD pipeline integration with practical YAML examples.
GuideComplete Guide to Docker in Production
Run Docker containers in production with confidence. Covers multi-stage builds, distroless images, health checks, image scanning, resource limits, logging, secrets, multi-arch builds, and container runtime security with practical Dockerfile examples.
GuideSentry: Error Tracking, Triage, and Resolution
Master Sentry for production error tracking. Covers SDK integration in Python, Node.js, Java, release tracking, source maps, performance monitoring, and alerting.