Monitoring & Observability
Sentry, Datadog, Splunk, Prometheus, Grafana, ELK, distributed tracing, and intelligent alerting
Observability: See What's Happening Inside Your Systems
You can't fix what you can't see. Monitoring tells you when something breaks. Observability tells you why. In production, you need both: real-time metrics to track system health, logs to debug issues, traces to follow requests through distributed systems, and intelligent alerts to wake you up only when it matters. This lesson covers the industry-standard tools, Sentry for error tracking, Prometheus + Grafana for metrics, ELK for logs, Datadog for all-in-one observability, and how to implement distributed tracing. All with Python integration examples.
The Three Pillars of Observability
Modern observability is built on three pillars: metrics, logs, and traces. Each serves a different purpose.
Metrics
What: Numerical data over time
Questions: How much? How fast?
Examples:
• Request rate (req/sec)
• Error rate (%)
• Latency (ms)
• CPU/Memory usage
Tools: Prometheus, Datadog, CloudWatch
Logs
What: Discrete events with context
Questions: What happened?
Examples:
• Application logs
• Error messages
• Audit trails
• Debug information
Tools: ELK, Splunk, Loki, Datadog
Traces
What: Request journey through system
Questions: Where did it go? How long?
Examples:
• API → Service A → DB
• Service mesh paths
• Distributed transactions
• Performance bottlenecks
Tools: Jaeger, Zipkin, Datadog APM
Observability in Action: 1. METRICS tell you WHAT is wrong "Error rate spiked to 15% at 2:30 AM" 2. LOGS tell you WHY it's wrong "DatabaseConnectionError: Cannot connect to postgres:5432" 3. TRACES tell you WHERE it went wrong "Request spent 5s in payment-service → database query timeout" Together: Complete picture of system behavior
Error Tracking, Sentry
Sentry captures exceptions, tracks errors, and groups them intelligently. It's the industry standard for error monitoring.
What Sentry Does
- Captures exceptions automatically
- Groups similar errors together
- Tracks release versions
- Shows user impact (how many users affected)
- Provides stack traces with context
- Source map integration for frontend
- Performance monitoring
When to Use Sentry
- Production error monitoring
- Track application exceptions
- User-facing applications
- Need error grouping and deduplication
- Want context (user, environment, breadcrumbs)
- Multi-language support needed
Python Integration
# Install Sentry SDK
pip install sentry-sdk
# Basic setup
import sentry_sdk
# Initialize Sentry
sentry_sdk.init(
dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
traces_sample_rate=1.0, # 100% of transactions for tracing
profiles_sample_rate=1.0, # Profile 100% of sampled transactions
environment="production",
release="myapp@1.2.3",
)
# Automatic exception capture
def divide(a, b):
return a / b
try:
result = divide(10, 0)
except Exception as e:
# Sentry automatically captures this
sentry_sdk.capture_exception(e)
# Manual error reporting
sentry_sdk.capture_message("Something went wrong", level="error")
# Add context
sentry_sdk.set_user({"id": 123, "email": "user@example.com"})
sentry_sdk.set_tag("payment_method", "stripe")
sentry_sdk.set_context("order", {"order_id": "ORDER-123", "total": 99.99})
# Breadcrumbs (what happened before error)
sentry_sdk.add_breadcrumb(
category="auth",
message="User logged in",
level="info"
)# Flask integration
from flask import Flask
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn="https://...",
integrations=[FlaskIntegration()],
traces_sample_rate=0.1, # 10% sampling in production
environment="production",
)
app = Flask(__name__)
@app.route('/api/users/<user_id>')
def get_user(user_id):
# Sentry automatically creates transactions for each request
user = db.query(User).get(user_id)
if not user:
# This will be captured by Sentry
raise ValueError(f"User {user_id} not found")
return {"user": user.to_dict()}# Django integration
# settings.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
sentry_sdk.init(
dsn="https://...",
integrations=[DjangoIntegration()],
traces_sample_rate=0.1,
send_default_pii=True, # Send user IP, user agent
environment="production",
release="myapp@1.2.3",
)
# views.py
from django.http import JsonResponse
from sentry_sdk import capture_message, set_tag
def checkout(request):
set_tag("payment_provider", "stripe")
try:
result = process_payment(request.POST)
return JsonResponse({"success": True})
except PaymentError as e:
# Automatically captured by Sentry
capture_message("Payment failed", level="error")
return JsonResponse({"error": str(e)}, status=400)• Set environment (dev/staging/prod) to filter errors
• Tag releases to track regressions
• Add user context for user-specific debugging
• Use breadcrumbs to understand user journey
• Sample transactions in production (10-20%) to save quota
• Set up alerts for new/regression issues
Metrics & Monitoring, Prometheus + Grafana
Prometheus collects and stores metrics. Grafana visualizes them. Together, they're the open-source standard for metrics monitoring.
Prometheus
Role: Metrics collection & storage
- Time-series database
- Pull-based scraping
- PromQL query language
- Alerting rules
- Service discovery
Grafana
Role: Visualization & dashboards
- Beautiful dashboards
- Multiple data sources
- Templating variables
- Alerting (optional)
- Plugin ecosystem
Prometheus Metric Types
| Type | Description | Use Case |
|---|---|---|
| Counter | Always increases (resets on restart) | Total requests, errors, bytes sent |
| Gauge | Can go up or down | Current memory usage, active connections, queue size |
| Histogram | Samples observations (buckets) | Request duration, response size |
| Summary | Similar to histogram, calculates quantiles | Request duration percentiles |
Python Integration with prometheus_client
# Install
pip install prometheus-client
# Basic metrics in Python
from prometheus_client import Counter, Histogram, Gauge, Summary
from prometheus_client import start_http_server
import time
import random
# Define metrics
http_requests_total = Counter(
'http_requests_total',
'Total HTTP requests',
['method', 'endpoint', 'status']
)
http_request_duration_seconds = Histogram(
'http_request_duration_seconds',
'HTTP request latency',
['method', 'endpoint']
)
active_connections = Gauge(
'active_connections',
'Number of active connections'
)
database_query_duration = Summary(
'database_query_duration_seconds',
'Database query duration'
)
# Use metrics
def handle_request(method, endpoint):
# Increment counter
http_requests_total.labels(method=method, endpoint=endpoint, status=200).inc()
# Track duration with histogram
with http_request_duration_seconds.labels(method=method, endpoint=endpoint).time():
time.sleep(random.random() * 0.1) # Simulate work
# Update gauge
active_connections.inc()
# ... do work ...
active_connections.dec()
# Track database query
with database_query_duration.time():
# Execute query
time.sleep(random.random() * 0.05)
# Start metrics server on :8000/metrics
if __name__ == '__main__':
start_http_server(8000)
# Simulate traffic
while True:
handle_request('GET', '/api/users')
handle_request('POST', '/api/orders')
time.sleep(1)# Flask integration with automatic instrumentation
from flask import Flask
from prometheus_flask_exporter import PrometheusMetrics
app = Flask(__name__)
# Automatic instrumentation
metrics = PrometheusMetrics(app)
# Custom metrics
by_endpoint_counter = metrics.counter(
'by_endpoint_counter',
'Request count by endpoint',
labels={'endpoint': lambda: request.endpoint}
)
@app.route('/api/users')
@by_endpoint_counter
def get_users():
return {"users": []}
@app.route('/api/orders')
def create_order():
# Automatically tracked
return {"order_id": 123}
# Custom metric
orders_created = Counter('orders_created_total', 'Total orders created')
@app.route('/api/checkout', methods=['POST'])
def checkout():
# ... process order ...
orders_created.inc()
return {"success": True}
# Metrics available at /metrics endpointPromQL Queries (for Grafana)
# Request rate (requests per second)
rate(http_requests_total[5m])
# Error rate percentage
rate(http_requests_total{status=~"5.."}[5m]) /
rate(http_requests_total[5m]) * 100
# 95th percentile latency
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m]))
# Average latency
rate(http_request_duration_seconds_sum[5m]) /
rate(http_request_duration_seconds_count[5m])
# Top 5 endpoints by request count
topk(5, sum by (endpoint) (rate(http_requests_total[5m])))
# Memory usage
process_resident_memory_bytes / 1024 / 1024 # Convert to MB
# CPU usage
rate(process_cpu_seconds_total[5m]) * 100
# Active connections trend
avg_over_time(active_connections[10m])1. App exposes /metrics endpoint (prometheus_client)
2. Prometheus scrapes /metrics every 15s
3. Grafana queries Prometheus for data
4. Dashboards visualize metrics in real-time
5. Alerts fire when metrics cross thresholds
Logging, ELK Stack (Elasticsearch, Logstash, Kibana)
ELK Stack is the industry standard for log aggregation and analysis. Collect logs from all services, search them in milliseconds, visualize patterns.
Elasticsearch
Role: Storage & search
- Distributed search engine
- Full-text search
- JSON documents
- Near real-time
Logstash
Role: Log processing
- Ingest from multiple sources
- Parse and transform
- Enrich with metadata
- Output to Elasticsearch
Kibana
Role: Visualization
- Search interface
- Log exploration
- Dashboards
- Alerting
Structured Logging in Python
# Install
pip install python-json-logger elasticsearch
# Structured logging with JSON output
import logging
from pythonjsonlogger import jsonlogger
# Setup JSON formatter
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(name)s %(levelname)s %(message)s'
)
logHandler.setFormatter(formatter)
logger.addHandler(logHandler)
# Log with structured data
logger.info(
"User login",
extra={
"user_id": 12345,
"email": "user@example.com",
"ip_address": "192.168.1.1",
"login_method": "oauth"
}
)
# Output (JSON):
# {
# "asctime": "2024-01-20 10:30:00",
# "name": "root",
# "levelname": "INFO",
# "message": "User login",
# "user_id": 12345,
# "email": "user@example.com",
# "ip_address": "192.168.1.1",
# "login_method": "oauth"
# }# Send logs directly to Elasticsearch
from elasticsearch import Elasticsearch
import logging
import datetime
class ElasticsearchHandler(logging.Handler):
def __init__(self, es_host='localhost:9200', index_name='app-logs'):
super().__init__()
self.es = Elasticsearch([es_host])
self.index_name = index_name
def emit(self, record):
log_entry = {
'timestamp': datetime.datetime.now(datetime.timezone.utc).isoformat(),
'level': record.levelname,
'message': record.getMessage(),
'logger': record.name,
'path': record.pathname,
'line': record.lineno,
}
# Add extra fields
if hasattr(record, 'user_id'):
log_entry['user_id'] = record.user_id
if hasattr(record, 'request_id'):
log_entry['request_id'] = record.request_id
# Send to Elasticsearch
self.es.index(
index=f"{self.index_name}-{datetime.date.today().strftime('%Y.%m.%d')}",
document=log_entry
)
# Setup
logger = logging.getLogger()
logger.addHandler(ElasticsearchHandler())
# Use
logger.info("Order created", extra={"user_id": 123, "order_id": 456})# Flask with request logging
from flask import Flask, request, g
import logging
import uuid
app = Flask(__name__)
# Configure structured logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.before_request
def before_request():
# Generate request ID
g.request_id = str(uuid.uuid4())
# Log request
logger.info(
"Request started",
extra={
"request_id": g.request_id,
"method": request.method,
"path": request.path,
"ip": request.remote_addr,
"user_agent": request.headers.get('User-Agent')
}
)
@app.after_request
def after_request(response):
# Log response
logger.info(
"Request completed",
extra={
"request_id": g.request_id,
"status_code": response.status_code,
"content_length": response.content_length
}
)
return response
@app.route('/api/users/<user_id>')
def get_user(user_id):
logger.info(
"Fetching user",
extra={"request_id": g.request_id, "user_id": user_id}
)
return {"user_id": user_id}• Use structured logging (JSON) for easier searching
• Include request_id to correlate logs across services
• Index rotation (daily/weekly) to manage size
• Set retention policies (delete old logs)
• Use Filebeat instead of Logstash for lower resource usage
• Create index patterns in Kibana for easy filtering
All-in-One Platforms, Datadog & Splunk
Datadog and Splunk provide metrics, logs, and traces in a single platform. Expensive but comprehensive.
| Aspect | Datadog | Splunk |
|---|---|---|
| Focus | Modern apps, cloud-native, APM | Enterprise logs, security, SIEM |
| Strengths | Beautiful UI, easy setup, integrations | Powerful search, log analysis, compliance |
| Pricing | Per host/container, can be expensive | Per GB ingested, very expensive |
| Best For | Startups, SaaS, microservices | Large enterprises, security teams |
| Learning Curve | Easy | Steep (SPL query language) |
Datadog Python Integration
# Install
pip install ddtrace datadog
# APM (Application Performance Monitoring)
# Run your app with ddtrace
# ddtrace-run python app.py
from ddtrace import tracer
from datadog import initialize, statsd
# Initialize Datadog
initialize(api_key='your-api-key')
# Custom metrics
statsd.increment('page.views', tags=['page:home'])
statsd.gauge('users.active', 100)
statsd.histogram('request.duration', 0.5)
# Custom tracing
with tracer.trace("database.query") as span:
span.set_tag("query.type", "SELECT")
span.set_tag("table", "users")
result = db.execute("SELECT * FROM users")
# Distributed tracing (automatic with ddtrace-run)
# Traces follow requests across services# Flask with Datadog APM
from flask import Flask
from ddtrace import patch_all
# Patch all supported libraries
patch_all()
app = Flask(__name__)
@app.route('/api/users')
def get_users():
# Automatically traced by Datadog
return {"users": []}
# Run with: ddtrace-run flask run
# Datadog automatically:
# - Traces HTTP requests
# - Traces database queries (if using supported ORMs)
# - Captures errors
# - Measures latency• Need metrics + logs + traces in one place
• Want minimal setup and maintenance
• Budget allows (can be $$$)
• Need vendor support
When to use open-source stack:
• Tight budget
• Want full control
• Self-host everything
• Have DevOps team to maintain
Distributed Tracing, OpenTelemetry, Jaeger
In microservices, a single request touches multiple services. Distributed tracing shows the complete journey and where time is spent.
Distributed Trace Example:
User Request → API Gateway (5ms)
↓
Auth Service (20ms)
↓
Order Service (150ms)
├→ Inventory Service (30ms)
│ └→ Database Query (25ms)
├→ Payment Service (100ms)
│ ├→ Stripe API (80ms)
│ └→ Database Query (15ms)
└→ Email Service (10ms)
└→ SendGrid API (8ms)
Total: 175ms (visible in single trace)
Bottleneck: Payment Service → Stripe API (80ms)
Each span has:
- Service name
- Operation name
- Start time, duration
- Tags (metadata)
- Logs (events during span)OpenTelemetry Python (Industry Standard)
# Install OpenTelemetry
pip install opentelemetry-api opentelemetry-sdk pip install opentelemetry-exporter-otlp-proto-grpc pip install opentelemetry-instrumentation-flask pip install opentelemetry-instrumentation-requests
# Setup OpenTelemetry tracing
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
# Setup tracer
resource = Resource.create({"service.name": "my-service"})
trace.set_tracer_provider(TracerProvider(resource=resource))
tracer = trace.get_tracer(__name__)
# Export to Jaeger via OTLP (Jaeger natively supports OTLP)
otlp_exporter = OTLPSpanExporter(
endpoint="localhost:4317",
insecure=True,
)
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(otlp_exporter)
)
# Create custom spans
def process_order(order_id):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order_id", order_id)
# Child span
with tracer.start_as_current_span("validate_order"):
validate(order_id)
with tracer.start_as_current_span("charge_payment"):
charge_payment(order_id)
with tracer.start_as_current_span("send_confirmation"):
send_email(order_id)
span.set_attribute("status", "completed")
return True# Flask auto-instrumentation
from flask import Flask
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
app = Flask(__name__)
# Auto-instrument Flask
FlaskInstrumentor().instrument_app(app)
# Auto-instrument requests library
RequestsInstrumentor().instrument()
@app.route('/api/users/<user_id>')
def get_user(user_id):
# This HTTP request is automatically traced
# Calls to external services are also traced
import requests
response = requests.get(f"http://auth-service/verify/{user_id}")
return {"user_id": user_id}
# Every request automatically creates a trace
# with spans for HTTP request, database queries, external calls• Jaeger: Open-source, CNCF project, good UI
• Zipkin: Open-source, simpler than Jaeger
• Datadog APM: Commercial, great UX, expensive
• AWS X-Ray: AWS-native, easy AWS integration
• Tempo: Grafana's tracing backend
OpenTelemetry is vendor-neutral, works with all of them
Alerting, Smart Notifications
Good alerting wakes you up for real issues, not noise. Bad alerting causes alert fatigue.
✅ Good Alerts
- Actionable (you can do something)
- Clear threshold (error rate >5%)
- Contextual (which service, why)
- Appropriate urgency (page vs email)
- Symptom-based (user impact)
- Few false positives
❌ Bad Alerts
- Not actionable (informational)
- Vague (something is wrong)
- Too sensitive (flapping)
- Alert fatigue (100+ alerts/day)
- Cause-based (CPU high) not symptom
- Missing context
Alert Severity Levels
| Level | When to Use | Channel | Example |
|---|---|---|---|
| 🔴 Critical | Customer-facing outage, immediate action required | PagerDuty, phone | API error rate >50% |
| 🟡 Warning | Degraded but functioning, investigate soon | Slack, email | Latency p95 >1s |
| 🔵 Info | Notable events, no action needed | Dashboard | Deployment completed |
Prometheus Alerting Rules
# alerts.yml
groups:
- name: api_alerts
interval: 30s
rules:
# High error rate
- alert: HighErrorRate
expr: |
rate(http_requests_total{status=~"5.."}[5m]) /
rate(http_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: "High error rate on {{ $labels.instance }}"
description: "Error rate is {{ $value | humanizePercentage }}"
runbook: "https://wiki.company.com/runbooks/high-error-rate"
# High latency
- alert: HighLatency
expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])
) > 1.0
for: 10m
labels:
severity: warning
team: backend
annotations:
summary: "High latency on {{ $labels.service }}"
description: "P95 latency is {{ $value }}s"
# Service down
- alert: ServiceDown
expr: up{job="api"} == 0
for: 2m
labels:
severity: critical
team: sre
annotations:
summary: "Service {{ $labels.instance }} is down"
description: "Cannot scrape metrics from {{ $labels.instance }}"
# Memory usage high
- alert: HighMemoryUsage
expr: |
(process_resident_memory_bytes /
node_memory_MemTotal_bytes) > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is {{ $value | humanizePercentage }}"Python: Send Alerts to Slack
# Python alerting helper
import requests
import json
class AlertManager:
def __init__(self, slack_webhook_url):
self.slack_webhook_url = slack_webhook_url
def send_alert(self, severity, title, message, context=None):
"""Send alert to Slack"""
# Color based on severity
colors = {
'critical': '#DC2626', # Red
'warning': '#F59E0B', # Yellow
'info': '#3B82F6' # Blue
}
payload = {
"attachments": [{
"color": colors.get(severity, '#6B7280'),
"title": f"[{severity.upper()}] {title}",
"text": message,
"fields": [],
"footer": "Monitoring System",
"ts": int(time.time())
}]
}
# Add context fields
if context:
for key, value in context.items():
payload["attachments"][0]["fields"].append({
"title": key,
"value": str(value),
"short": True
})
response = requests.post(
self.slack_webhook_url,
data=json.dumps(payload),
headers={'Content-Type': 'application/json'}
)
return response.status_code == 200
# Usage
alert_manager = AlertManager(slack_webhook_url="https://hooks.slack.com/...")
# Send critical alert
alert_manager.send_alert(
severity='critical',
title='API Error Rate Spike',
message='Error rate exceeded 5% threshold',
context={
'Service': 'payment-api',
'Error Rate': '12.5%',
'Duration': '5 minutes',
'Affected Users': '~500'
}
)
# Send warning
alert_manager.send_alert(
severity='warning',
title='High Database Latency',
message='Database queries are slower than normal',
context={
'Service': 'postgres-primary',
'P95 Latency': '850ms',
'Normal': '200ms'
}
)• PagerDuty: On-call rotation, escalation, incident management
• Slack: Team notifications, context-rich messages
• Email: Non-urgent alerts, daily summaries
• OpsGenie: Alternative to PagerDuty
• Webhook: Custom integrations
Best Practice: Route by severity (critical → PagerDuty, warning → Slack)
Complete Tool Comparison
| Tool | Type | Best For | Pricing | Setup Complexity |
|---|---|---|---|---|
| Sentry | Error tracking | Application errors, exceptions | Free tier, $26+/mo | ⭐ Very Easy |
| Prometheus | Metrics | Time-series metrics, K8s monitoring | Free (open-source) | ⭐⭐⭐ Medium |
| Grafana | Visualization | Dashboards, metrics visualization | Free (open-source) | ⭐⭐ Easy |
| ELK Stack | Logs | Log aggregation, search, analysis | Free (self-host), Elastic Cloud $$$ | ⭐⭐⭐⭐ Complex |
| Datadog | All-in-one | Metrics + logs + traces, cloud-native | $$$$ (~$15/host/mo) | ⭐ Very Easy |
| Splunk | Logs/SIEM | Enterprise logging, security, compliance | $$$$$ (~$150/GB/mo) | ⭐⭐⭐⭐ Complex |
| Jaeger | Distributed tracing | Microservices tracing, OpenTelemetry | Free (open-source) | ⭐⭐⭐ Medium |
| New Relic | All-in-one APM | Application performance, legacy apps | $$$$ (~$99+/user/mo) | ⭐⭐ Easy |
Recommended Stacks by Use Case
🚀 Startup Stack (Low Cost, Quick Setup)
Error Tracking: Sentry (free tier)
Metrics: Prometheus + Grafana Cloud (free tier)
Logs: Loki + Grafana (free, lightweight)
APM: Sentry Performance (if budget allows)
Total Cost: $0-50/month
Setup Time: 1-2 days
Maintenance: Low
Scales to: 10-50 servers
📈 Growing Company (Balanced)
Error Tracking: Sentry
Metrics: Prometheus + Grafana (self-hosted)
Logs: ELK Stack or Datadog Logs
APM: Datadog APM or Jaeger
Total Cost: $500-2000/month
Setup Time: 1-2 weeks
Maintenance: Medium
Scales to: 100-500 servers
🏢 Enterprise (Full Featured)
All-in-One: Datadog (metrics + logs + APM + RUM)
Or: Splunk (security teams)
Error Tracking: Sentry
Alerting: PagerDuty
Total Cost: $5,000-50,000+/month
Setup Time: 1-2 months
Maintenance: Low (vendor managed)
Scales to: Unlimited
🔓 100% Open-Source Stack
Metrics: Prometheus + Grafana
Logs: Loki + Grafana or ELK
Tracing: Jaeger or Tempo
Alerting: Alertmanager → Slack
Total Cost: Infrastructure only
Setup Time: 2-4 weeks
Maintenance: High (DIY)
Scales to: Depends on infra
Key Takeaways
- Three Pillars: Metrics (how much), Logs (what happened), Traces (where it went)
- Sentry: Error tracking, exception monitoring, performance monitoring
- Prometheus + Grafana: Open-source metrics, powerful queries, beautiful dashboards
- ELK Stack: Log aggregation, full-text search, visualization
- Datadog: All-in-one platform, easy setup, expensive but comprehensive
- Splunk: Enterprise logging, SIEM, security focus, very expensive
- Distributed Tracing: OpenTelemetry + Jaeger for microservices observability
- Alerting: Actionable, severity-based, right channel (PagerDuty vs Slack)
- Python Integration: prometheus_client, sentry_sdk, OpenTelemetry auto-instrumentation
- Start Simple: Sentry + Grafana Cloud, add complexity as you grow
Observability Checklist
Essential
- Error tracking (Sentry)
- Application metrics (Prometheus)
- Request logging with IDs
- Dashboard for key metrics
- Alerts for critical errors
- Health check endpoints
Advanced
- Distributed tracing (Jaeger)
- Log aggregation (ELK)
- On-call rotation (PagerDuty)
- SLO/SLA monitoring
- User session replay
- Synthetic monitoring