Structured logs are machine-readable records, usually one JSON object per line, with fields for level, message and context. They let you filter and aggregate instead of grepping free-text, and a correlation id ties one request’s lines together across services.
Before you start
You should be comfortable with console and async request handling. This article covers log shape and context; it does not cover a specific logging library.
Step-by-step walkthrough
Step 1: Emit one JSON object per event
Write a single line of JSON with a level, a stable message, and structured fields. Levels (debug, info, warn, error) let you filter by severity at the collector, and a stable message lets you alert on it without parsing variable text.
Step 2: Attach a correlation id
Generate an id at the request entry point and include it on every log line for that request, including in calls to downstream services. Correlating logs by id is what makes a distributed trace readable, and it survives across services if you propagate the id in a header.
Step 3: Redact and bound what you log
Never log passwords, tokens, or full personal data. Log identifiers instead of payloads, and cap large fields. High-cardinality values such as a raw URL with an id belong in a field, not in the message, so metrics do not explode.
Worked scenario
A tiny logger writes JSON lines with a request id.
function log(level, message, fields = {}) {
process.stdout.write(`${JSON.stringify({ level, message, ...fields })}\n`);
}
log('info', 'request.completed', { requestId: 'r-1', route: '/health', status: 200 });Walk through the example
Each call writes one JSON object, so a collector can parse it and index by level, requestId and status. The message is stable (request.completed) while the variable data lives in fields, which keeps both alerting and search predictable. Writing to stdout is the convention for containers, where the platform ships logs.
Common mistake
Using console.log with interpolated strings, which mixes data into an unparsable message. Another is logging an entire request body, which leaks secrets and personal data into a system with broad access.
Verify the behavior
Parse the emitted lines as JSON and confirm each has a level, message and expected fields. Trace one request through logs by its correlation id and confirm every line carries it. Grep the output for known secret fields and confirm none appear.
Interview exercise
Why prefer a stable message with fields over a fully formatted string?
Answer and reasoning
A stable message is a low-cardinality value you can count and alert on, such as request.completed, while the variable parts live in fields you can query. A formatted string changes with every request, which breaks aggregation and makes alerts brittle. Separating the two keeps both metrics and search useful.
Continue learning
Compare context propagation in Async local context and Distributed tracing. Read the Node.js process stdout documentation and try the Node.js interview questions.