Structured logging in your app

Reading · 7 min · Module 2, lesson 2 of 547 min left in this module

Module 2 · LogsLesson 2 of 5

Goal: Write JSON log lines with the fields that make a problem easy to find.

Key idea

Write each log line as one JSON object with the same fields every time: a level, a message, and the facts you'd want when something breaks, such as a request ID, the route, the status and how long it took. Lines like that can be searched, filtered and read by tools as well as people.

From sentences to fields

A plain log line is a sentence:

2026/09/29 12:22:46 GET /products failed after 0ms

It's readable, but every developer words it differently, and finding "all failed requests to /products" means guessing the phrasing. The structured version of the same event, straight from the learn-shop-api sample this module uses:

{"time":"2026-09-29T12:22:46.917Z","level":"error","msg":"request failed","request_id":"4beed50a3d0b5c6a","method":"GET","route":"/products","status":500,"duration_ms":0,"error":"simulated failure (ERROR_RATE=0.3)"}

One line, one object, fields in the same place every time. The shop logs its start-up the same way: starting learn-shop-api with its settings, then ready: catalog loaded, /healthz answers 200, then listening. When it can't start, the last line says why, in a field you can search for.

The fields worth having

  • level: error when something failed, warn for trouble that didn't fail, info for normal events, debug for detail you only want sometimes. The shop logs a 404 as warn and a 500 as error.
  • msg: a short, fixed message, like request failed. Put the changing values in fields, not in the message.
  • request_id: a value that ties every line about one request together.
  • route, status, duration_ms: what was asked for, how it ended, how long it took.
  • error: the error itself, on failures only.

And one field you never write: a secret. Log that a key is set, never its value. The shop's start-up line has "signing_key_set":true, not the key.

Request IDs

The shop reads an X-Request-Id header, or makes an ID up, and returns it in the response. When a user reports a failure, the ID from their response finds every line about that request. With several services, pass the same ID along to each one: it's a lightweight stand-in for tracing.

Searching structured logs

Log search on ComputeSphere matches the exact text you type, ignoring case, so every field is searchable as text. Search request failed to find the failures, a request ID to follow one request, or a field with its value, such as "route":"/products". A level field also helps the console's Error, Warn, Info and Debug filters sort your lines.

How do I log JSON in my language?

Most languages have a logger that does it for you:

  • Go: log/slog with slog.NewJSONHandler(os.Stdout, nil).
  • Node.js: pino.
  • Python: structlog, or logging with a JSON formatter.
  • Java: Logback or Log4j 2 with a JSON layout.

Point it at standard output, not a file.

Check yourself

Which log line will be easiest to find later?
A user reports that one checkout failed. What in your logs finds every line about it?

In the docs