Skip to content
daniel@cosenza:~/blog
SRE & DevOpsDeep Dive April 8, 2026 4 min read

Observability in Cloud-Native Systems: Metrics, Logs, and Traces

How the three pillars of observability complement each other, and why having all three matters more than maximizing any single one.

“Monitoring” tells you whether something is wrong; observability is about being able to ask arbitrary new questions about a system’s internal state without shipping new code to answer them. In cloud-native systems — where a single request might cross a dozen services — that distinction matters enormously, and it’s usually built from three complementary signal types: metrics, logs, and traces.

Metrics: cheap, aggregated, great for alerting

A metric is a numeric measurement recorded over time — request count, latency, memory usage — typically pre-aggregated (a counter, a gauge, a histogram) rather than stored as individual raw events. Prometheus is the de facto standard in cloud-native environments, scraping metrics endpoints on a fixed interval:

http_requests_total{method="GET", status="200"} 84213
http_request_duration_seconds_bucket{le="0.5"} 79231
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

Metrics are cheap to store and query at scale precisely because they’re aggregated — you lose the ability to inspect any individual request, but gain the ability to cheaply track trends and alert on thresholds across millions of events.

Logs: detailed, per-event, expensive at scale

A log is a discrete, timestamped record of a specific event, typically with unstructured or semi-structured detail — an error message, a stack trace, a specific request’s parameters. Structured logging (emitting JSON rather than free-text) is what makes logs actually queryable at scale rather than requiring regex archaeology:

{"timestamp": "2026-04-08T14:23:01Z", "level": "error", "service": "checkout", "trace_id": "abc123", "msg": "payment gateway timeout", "user_id": "u_9182"}
{service="checkout"} |= "error" | json | trace_id="abc123"

Logs answer “what exactly happened” for a specific event in a way aggregated metrics fundamentally can’t, at the cost of considerably higher storage and query cost at high volume — which is why log sampling and retention policies matter far more in practice than they do for metrics.

Traces: following one request across service boundaries

A trace follows a single request as it flows through multiple services, composed of spans — one per unit of work (an HTTP call, a database query) — each carrying a start time, duration, and parent-child relationship to other spans in the same trace:

Trace: abc123
├─ span: api-gateway (12ms)
│  └─ span: checkout-service (340ms)
│     ├─ span: inventory-check (45ms)
│     └─ span: payment-gateway-call (280ms)  ← the slow one
# OpenTelemetry instrumentation, conceptually
tracer.start_span("payment-gateway-call")

This is the signal that answers “which specific service in this request’s path is actually slow” — a question metrics alone (which are per-service, not per-request) and logs alone (which lack the causal parent-child linkage across services) can’t answer nearly as directly.

OpenTelemetry: one instrumentation standard for all three

OpenTelemetry (OTel) has become the standard vendor-neutral instrumentation layer for emitting metrics, logs, and traces from application code, decoupling “how do I instrument my code” from “which backend do I send this to” — the same application instrumentation can export to Prometheus, Jaeger, Datadog, or any other OTel-compatible backend without changing the instrumentation itself:

receivers:
  otlp:
    protocols:
      grpc:
      http:
exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
  otlp/jaeger:
    endpoint: "jaeger:4317"

An OpenTelemetry Collector typically sits between instrumented applications and backend storage, receiving all three signal types over a common protocol (OTLP) and routing/processing/exporting them onward.

Why you need all three, not just the “best” one

A common instinct is to ask which signal is most valuable and invest there exclusively — but each answers a genuinely different question, and incidents routinely require moving between all three: metrics first surface that latency spiked cluster-wide (an alert fires); traces narrow down which specific service in the request path is responsible; logs from that specific service, filtered by the trace ID the trace provided, reveal the actual error causing the slowdown. Correlating all three via a shared trace_id (propagated through logs and linked from the trace view) is what turns “three separate tools” into one coherent investigation workflow rather than three disconnected data sources.

1. Alert: p99 latency > 2s (metric)
2. Trace shows payment-gateway-call span consistently slow (trace)
3. Logs for that service, filtered by trace_id, show connection pool exhaustion (log)

The cost dimension: sampling and retention

At high request volume, capturing every trace and every log line in full detail becomes prohibitively expensive — sampling (recording only a percentage of traces, or all traces above a latency/error threshold) and tiered retention (recent data at full fidelity, older data aggregated or discarded) are standard practical necessities, not shortcuts. A well-designed observability strategy samples deliberately rather than either drowning in cost or silently missing the specific slow/failing requests that matter most.

The underlying principle

The “three pillars” framing is really about matching the right signal to the right question: aggregated trends and alerting (metrics), the exact detail of one specific event (logs), and causal, cross-service request flow (traces). A system instrumented with only one of the three can tell you that something’s wrong or what happened in isolation, but reliably answering why, across a distributed, multi-service request path, is what actually needs all three working together.