December 28, 2023 · 5 min read
Observability Basics: Logs, Metrics, and Traces
A practical introduction to the three pillars of observability and how to implement them effectively in your infrastructure.
December 28, 2023 · 5 min read
A practical introduction to the three pillars of observability and how to implement them effectively in your infrastructure.
Observability isn't about collecting more data—it's about asking questions you didn't know you'd need to ask. Here's how to build a foundation that actually helps you debug production issues.
Logs are discrete events with context. They tell you what happened at a specific point in time.
{
"timestamp": "2024-01-15T10:23:45.123Z",
"level": "error",
"service": "payment-api",
"trace_id": "abc123",
"user_id": "user_456",
"message": "Payment processing failed",
"error": "Card declined",
"amount": 99.99,
"currency": "USD"
}
Best Practices:
Metrics are numerical measurements aggregated over time. They tell you how your system is performing.
# Counter: Total requests
http_requests_total{method="POST", path="/api/orders", status="200"} 15420
# Gauge: Current connections
database_connections_active{pool="primary"} 23
# Histogram: Request latency distribution
http_request_duration_seconds_bucket{le="0.1"} 8000
http_request_duration_seconds_bucket{le="0.5"} 9500
http_request_duration_seconds_bucket{le="1.0"} 9800
Key Metrics to Track:
Traces follow a request through your distributed system, showing you where time is spent.
Trace ID: abc123
├── frontend-web (32ms)
│ └── api-gateway (28ms)
│ ├── auth-service (5ms)
│ └── order-service (20ms)
│ ├── inventory-check (8ms)
│ └── database-query (10ms)
Transform unstructured logs to structured:
# Before: Hard to parse, no context
logger.info(f"User {user_id} placed order {order_id}")
# After: Structured with context
logger.info(
"Order placed",
extra={
"user_id": user_id,
"order_id": order_id,
"amount": amount,
"trace_id": get_trace_id()
}
)
Implement the RED method for every service:
from prometheus_client import Counter, Histogram
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.01, 0.05, 0.1, 0.5, 1.0, 5.0]
)
Propagate trace context across services:
from opentelemetry import trace
from opentelemetry.propagate import inject, extract
tracer = trace.get_tracer(__name__)
# Start a span
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
span.set_attribute("order.amount", amount)
# Propagate context to downstream services
headers = {}
inject(headers)
response = requests.post(
"http://inventory/check",
headers=headers
)
The real power comes from connecting logs, metrics, and traces:
{
"timestamp": "2024-01-15T10:23:45.123Z",
"level": "error",
"trace_id": "abc123", // Links to trace
"span_id": "span456", // Specific operation
"service": "payment-api",
"message": "Payment failed"
}
With this trace_id, you can:
| Pillar | Tool | Notes |
|---|---|---|
| Logs | Loki | Pairs well with Grafana |
| Metrics | Prometheus | Industry standard |
| Traces | Jaeger or Tempo | Tempo if using Loki |
| Visualization | Grafana | Unified dashboards |
| Pillar | AWS | GCP | Agnostic |
|---|---|---|---|
| Logs | CloudWatch | Cloud Logging | Datadog |
| Metrics | CloudWatch | Cloud Monitoring | Datadog |
| Traces | X-Ray | Cloud Trace | Datadog |
More logs != better observability. Log meaningful events, not every function call.
# Bad: Noise
logger.debug("Entering function")
logger.debug("Variable x = 5")
logger.debug("Exiting function")
# Good: Meaningful events
logger.info("Payment processed", extra={"amount": 100, "user_id": "123"})
High-cardinality labels explode metric storage:
# Bad: user_id has millions of unique values
REQUEST_COUNT = Counter(
'requests_total',
['method', 'endpoint', 'user_id'] # Don't do this
)
# Good: Keep cardinality bounded
REQUEST_COUNT = Counter(
'requests_total',
['method', 'endpoint', 'status']
)
Tracing 100% of requests is expensive. Sample intelligently:
# Sample 10% of requests, but always trace errors
sampler = TraceIdRatioBased(0.1) # 10% sampling
Good alerts focus on user impact:
# Good: Alerts on user-facing symptoms
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m])) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate above 1%"
# Bad: Alerts on implementation details
- alert: DatabaseConnectionsHigh
expr: database_connections > 80
Observability isn't a product you buy—it's a practice you build. Start with the basics, iterate, and expand as your understanding of your system grows.