Logs vs metrics vs traces
Published
When a patient walks into a hospital, three kinds of record help. The vital signs monitor shows a few numbers continuously: pulse, blood pressure, oxygen. It beeps when one goes out of range. That is metrics. The case notes are detailed, timestamped entries written as things happen: "14:02, complained of chest pain, given aspirin". That is logs. And the patient's path through triage, X-ray, cardiology and the pharmacy, with how long each step took, is a trace.
Each answers a different question. Metrics: is something wrong? Traces: where in the system is it wrong? Logs: what exactly happened, and why?
An alert fires: checkout p99 latency went from 300 ms to 2 s. You have 30 microservices. Which of the three would you open first, which second, and what would you look for in each?
Metrics: cheap numbers over time#
A metric is a number measured repeatedly and aggregated: requests per second, error rate, p99 latency, queue depth, CPU. Three basic types:
- Counter: only goes up (requests served, errors). You graph its rate.
- Gauge: goes up and down (memory in use, connections open, queue length).
- Histogram: counts values into buckets, so you can compute percentiles (latency).
Because a metric is just a number per time interval, storing a year of it is cheap no matter how much traffic you serve, and querying it is fast. That makes metrics the right basis for dashboards and alerts. Prometheus, Datadog, CloudWatch and Grafana Mimir store them.
The trap is cardinality. Each unique combination of label values is a separate time
series. Labels like endpoint, status and region are fine. Add user_id and you have
created one series per user, and your metrics system falls over or your bill explodes.
Tens of thousands of series is normal and cheap. Keep label values bounded: endpoint templates like /orders/:id, never raw paths with ids in them.
Logs: detailed events, expensive at volume#
A log line is a record of one event, with as much detail as you like: the order id, the coupon code, the exception and stack trace. Logs are where the why lives. Write them structured (JSON with consistent field names), not as free text, so they can be filtered and aggregated:
{"ts":"2026-10-18T20:00:01.412Z","level":"warn","service":"inventory","trace_id":"4bf92f35","order_id":8812,"msg":"reserve slow","duration_ms":240,"db_calls":6}The cost is volume. A service at 5,000 requests a second writing five lines per request is two billion lines a day. Storage, indexing and search at that volume is often the largest line on an observability bill. So teams set levels carefully, sample noisy debug logs, keep hot storage short (days) and archive the rest cheaply. Elasticsearch or OpenSearch, Loki, Datadog, Splunk and CloudWatch Logs are common stores.
Traces: one request's path#
A trace follows a single request through every service it touches. Each unit of work is a
span with a start time, a duration, a parent and attributes. The trace id travels between
services in a header (the W3C traceparent standard), so spans from thirty services join into
one tree. Traces answer the question metrics and logs cannot: where did the time go?
Here is one slow checkout. Click a span to see its attributes and the log lines attached to it, then apply the fixes.
Tracing every request is expensive, so most systems sample: keep 1% of normal traces, but tail sampling keeps every trace that was slow or failed, which are the ones you want. Jaeger, Zipkin, Grafana Tempo, Honeycomb and Datadog APM store traces.
Together#
The pillars work best joined up, and the glue is the trace id. Put it in every log line, attach trace ids to metric samples as exemplars, and an investigation flows:
- A metric alert fires: checkout p99 is 2 s.
- Click an exemplar on the latency graph to open a slow trace: the time is in inventory, six sequential database calls.
- Jump from the span to its logs: the warning names the order and the query.
OpenTelemetry (OTel) is the vendor neutral standard for producing all three: one SDK per language, one wire protocol (OTLP), and a collector that sends data to whichever backend you choose. It is the safe default for new services in 2026.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Metrics | Cheap at any traffic, fast queries, ideal for dashboards, alerts and SLOs. | No detail about individual requests; high cardinality labels are dangerous. | Always. The first signal and the basis of alerting. |
| Logs | Full detail of individual events, any field, the "why". | Expensive at volume; useless unstructured; hard to follow across services without a trace id. | Errors, audit trails, business events, debugging specific requests. |
| Traces | Shows where time and errors happen across services, per request. | Instrumentation and context propagation in every service; sampling needed at scale. | Any system with more than a few services, latency investigations. |
- OpenTelemetryOpenTelemetry: one standard for all three signals
- PrometheusMetrics, pull based, PromQL
- GrafanaDashboards; Loki (logs), Tempo (traces), Mimir (metrics)
- JaegerOpen source tracing
- DatadogCommercial, all three in one product
- ElasticsearchLog search (the E in ELK)
In the interview#
Checkpoint#
1. An alert fires for high checkout latency across 30 services. What is the fastest path to the cause?
2. Why is user_id a bad metric label?
3. What does tail sampling do?
- Metrics: is it wrong? Traces: where? Logs: why?
- Metrics are cheap but keep labels bounded. Cardinality multiplies.
- Logs: structured JSON with a trace id. Volume is the cost.
- Traces: spans joined by a propagated trace id. Tail sample.
- OpenTelemetry for all three.
Metrics are numbers aggregated over time, counters, gauges and histograms, cheap at any traffic and the basis for dashboards, alerts and SLOs, as long as label cardinality stays bounded. Logs are detailed records of individual events; written as structured JSON they explain why something happened, but they are expensive at volume, so I use levels, sampling and short hot retention. Traces follow one request across services as a tree of spans joined by a trace id propagated in the traceparent header; they show where time and errors occur, and tail sampling keeps the slow and failed ones. They work best linked: a metric alert leads, through an exemplar, to a slow trace, whose span leads to the logs carrying the same trace id. I instrument new services with OpenTelemetry so the backend can change without touching code, and I always include a business metric like orders per minute.