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)
Sentry Best Practices:
• 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

TypeDescriptionUse Case
CounterAlways increases (resets on restart)Total requests, errors, bytes sent
GaugeCan go up or downCurrent memory usage, active connections, queue size
HistogramSamples observations (buckets)Request duration, response size
SummarySimilar to histogram, calculates quantilesRequest 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 endpoint

PromQL 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])
Prometheus + Grafana Workflow:
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}
ELK Best Practices:
• 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.

AspectDatadogSplunk
FocusModern apps, cloud-native, APMEnterprise logs, security, SIEM
StrengthsBeautiful UI, easy setup, integrationsPowerful search, log analysis, compliance
PricingPer host/container, can be expensivePer GB ingested, very expensive
Best ForStartups, SaaS, microservicesLarge enterprises, security teams
Learning CurveEasySteep (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
When to use all-in-one platforms:
• 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
Distributed Tracing Tools:
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

LevelWhen to UseChannelExample
🔴 CriticalCustomer-facing outage, immediate action requiredPagerDuty, phoneAPI error rate >50%
🟡 WarningDegraded but functioning, investigate soonSlack, emailLatency p95 >1s
🔵 InfoNotable events, no action neededDashboardDeployment 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'
    }
)
Alert Channels:
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

ToolTypeBest ForPricingSetup Complexity
SentryError trackingApplication errors, exceptionsFree tier, $26+/mo⭐ Very Easy
PrometheusMetricsTime-series metrics, K8s monitoringFree (open-source)⭐⭐⭐ Medium
GrafanaVisualizationDashboards, metrics visualizationFree (open-source)⭐⭐ Easy
ELK StackLogsLog aggregation, search, analysisFree (self-host), Elastic Cloud $$$⭐⭐⭐⭐ Complex
DatadogAll-in-oneMetrics + logs + traces, cloud-native$$$$ (~$15/host/mo)⭐ Very Easy
SplunkLogs/SIEMEnterprise logging, security, compliance$$$$$ (~$150/GB/mo)⭐⭐⭐⭐ Complex
JaegerDistributed tracingMicroservices tracing, OpenTelemetryFree (open-source)⭐⭐⭐ Medium
New RelicAll-in-one APMApplication 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
Infrastructure for EngineersLesson 10