Structured Logging & Log Aggregation

Observability

Chapter 7 · Structured Logging & Log Aggregation

Metrics are done. This chapter moves to the second pillar obs1-1 named — logs — and delivers the real tooling cloud2-4's own correlation-ID material assumed was already in place.

Structured vs. Unstructured Logs

# Unstructured 2026-07-13 10:32:01 ERROR Payment failed for user 4821 - timeout after 5000ms # Structured {"timestamp": "2026-07-13T10:32:01Z", "level": "error", "msg": "payment failed", "user_id": 4821, "duration_ms": 5000}

Unstructured logs are free text, meant for a human reading them one line at a time — extracting user_id or duration_ms afterward means writing a regex against the message, fragile and quick to break the moment someone tweaks the wording. Structured logs emit their fields directly, as data — every field is queryable and filterable from the moment it's written, with no parsing guesswork required downstream.

Log Aggregation — Why You Need a Central System

In a system with dozens of services across dozens of instances, logs scattered across that many individual machines are practically unsearchable during a real incident — nobody is going to SSH into thirty containers one at a time while something is on fire. A log aggregation system centralizes logs from every source into one searchable place, which is exactly the capability cloud2-4's own "first five minutes" incident-response technique assumed was already sitting there, ready to use.

Loki vs. the ELK/EFK Stack — Two Different Philosophies

ELK (Elasticsearch, Logstash, Kibana) — or EFK, swapping in Fluentd/Fluent Bit — indexes the full text of every log line in Elasticsearch, enabling powerful free-text search across any word, anywhere, at real storage and compute cost. Loki takes a deliberately different approach: it doesn't index log content at all — only labels, the exact same concept as obs1-2's own Prometheus labels, applied here to logs instead of metrics. The actual log lines are stored compressed, cheaply, and only scanned — not separately indexed — within whichever label-selected stream a query has already narrowed down. This is a deliberate design choice, not a coincidence: Loki is built by the same team behind Grafana specifically to pair with Prometheus's own label-based philosophy, at a fraction of the resource cost full-text indexing requires.

LogQL — Loki's Own Query Language

{job="checkout-service"} |= "timeout" # Extracting and filtering on a structured field {job="checkout-service"} | json | duration_ms > 5000

{job="checkout-service"} is the label selector — cheap, indexed, exactly like a PromQL selector from obs1-4. |= "timeout" is a line filter, applied only within that already-narrowed stream — not indexed, but only scanning a small, pre-selected subset rather than every log line ever written. | json parses each structured line and exposes its fields for further filtering, as in the duration_ms > 5000 example — turning a structured field written once at log time into something directly queryable later.

Correlation IDs — Delivering On cloud2-4's Own Material

cloud2-4 named correlation IDs as the technique for tracing one request across multiple services' own logs, conceptually. Here's the concrete mechanism: a unique ID is generated once, at the edge — typically an API gateway — and passed along on every downstream service call, usually as an HTTP header, then included as a structured field in every log line each service emits while handling that request.

{job=~".+"} | json | request_id = "abc-123"

That single query, matching every job, filters down to every log line — across every service that touched this one request — sharing that exact request_id. This is the concrete technique that turns "find every log line related to this one failing request, across the whole system" from a conceptual goal into something you can actually run. obs1-8's traces are the even more powerful version of this same underlying idea — correlating not just log lines, but full per-service timing across the whole request.

Indexing approachBest for
ELK / EFKFull-text index of every log line's contentPowerful free-text search across unknown content
LokiLabels indexed only; content scanned within a narrowed streamCheap at scale, when you already know roughly which labels to filter by
Emit structured logs from day one, even on a small project
Retrofitting structured logging onto an already-large unstructured system is real, tedious work — every existing log statement has to be found and rewritten. Starting structured from the very first log line costs almost nothing extra and pays off the moment you need to query anything.
A correlation ID only works if every service actually propagates it
One service in the request's path that forgets to forward the incoming request ID header to the next service silently breaks the entire chain — with no error, no warning, just a gap in the trail where that ID stops appearing. This is a real, common integration gap, and it's usually only discovered during an actual incident, exactly when it's least convenient to find.

Hands-On Exercises

Exercise 1

Rewrite the unstructured log line "2026-07-13 09:15:44 WARN Rate limit exceeded for IP 203.0.113.7 on endpoint /api/checkout" as a structured JSON log line, and explain what becomes easier once it's structured.

📄 View solution
Exercise 2

Write a LogQL query that selects logs from the job "inventory-service" and filters to only lines containing the text "connection refused."

📄 View solution
Exercise 3

A request passes through an API gateway, an auth service, and a payments service, each logging with a request_id field — except the payments service, which was recently rewritten and doesn't include it. Explain exactly what breaks during an incident investigation, and why this failure is easy to miss until it matters.

📄 View solution

Chapter 7 Quick Reference

  • Structured logs (JSON key-value fields) are directly queryable; unstructured logs need fragile after-the-fact regex parsing
  • Log aggregation centralizes logs from every instance/service into one searchable place — required for cloud2-4's own incident-response technique to actually work
  • ELK/EFK — full-text indexes every log line's content; Loki — indexes labels only, scans content within the narrowed stream (Prometheus's own label philosophy, applied to logs)
  • LogQL: {label="value"} selects a stream (cheap, indexed); |= "text" filters lines; | json exposes structured fields for further filtering
  • A correlation/request ID, generated at the edge and propagated through every downstream call, is the concrete mechanism behind cloud2-4's own cross-service log correlation
  • One service dropping the ID silently breaks the whole trail — a real, common, easy-to-miss integration gap