1.Logs, metrics, and traces — three questions, not three formats
The three signals are not three ways of writing down the same thing. A **log** is a record of one event, with as much detail as you like — it answers "what happened in this request". A **metric** is a number aggregated over time, cheap to keep and cheap to query — it answers "is this happening more than usual". A **trace** follows one request across services and shows where its time went. You need all three because each is bad at the others' job: you cannot alert on logs affordably, you cannot debug a specific failure from a metric, and neither shows you which downstream service was slow.
Key features
- Metric cost grows with **label cardinality**, not traffic — never label with a user id, an order id, or a raw URL path
- Log cost grows with volume, and their value is per-event detail — the opposite trade-off
- Traces are the only signal that crosses a service boundary, and they are normally sampled
- Alert on metrics, debug in traces, confirm in logs — that ordering is what keeps each cost sane
- A shared `request_id` on all three is what makes them one system instead of three tools
- Django logs `django.request` at ERROR for 5XX and WARNING for 4XX, with `status_code` and `request` attached to the record
Which signal answers which question
- Three panels side by side, one per observability signal.
- LOGS: answers "what happened in this one request?", high detail, and its cost grows with event volume.
- METRICS (highlighted): answers "is it happening more than usual?", aggregated, and its cost grows with the number of label combinations rather than with traffic.
- TRACES: answers "where did the time go, across services?", normally sampled, and its cost grows with the sample rate.
- Below the three panels, a horizontal accent line marks the shared request_id field that joins them, with the working order: alerts fire on metrics, you debug in traces, and you confirm in logs.
Command
metrics.timing("db.query_ms", ms, tags={"view": view_name}) # bounded labels
log.warning("payment declined", extra={"order_id": order.id, "request_id": rid})Alert on metrics, debug in traces, confirm in logs — the ordering follows from what each one costs. Metric cost grows with label cardinality, so labels must come from a small fixed set: use the URL route, never the path; put ids on trace attributes and in structured log fields instead, where they are free. Traces are the only signal that crosses a service boundary. And give all three the same `request_id`, because that single shared field is the difference between one system and three search boxes.