Logging
Recording what a running server is doing in a structured, searchable way, so problems can be understood after the fact instead of only while watching a terminal.
What is it?
While you're actively developing, a stray console.log is often enough to see what's happening — you're watching the terminal right there. But a real production server runs unattended, often across multiple machines, for weeks at a time, handling requests you'll never personally watch happen. When something goes wrong at 3 AM, you need a record of what the server was doing at that moment — not to have been standing there watching.
Logging is the practice of deliberately recording events as a server runs — a request came in, a database call took 400ms, a payment failed — usually as structured, timestamped entries with a severity level (like debug, info, warn, error) attached, and often sent somewhere searchable rather than just printed to a terminal that disappears when the process restarts.
Explain like I'm 10
console.log is like shouting something out loud in an empty room — useful if you happen to be standing there listening at that exact moment, but gone forever otherwise. Structured logging is like a ship's logbook: every entry is timestamped, labeled by importance, and kept in a permanent, searchable record that anyone can review later, even long after the moment has passed.
Examples
Ad-hoc console.log vs. structured logging
// Ad-hoc — fine for local debugging, poor for production
console.log("user logged in", userId);
// Structured — with a logging library like winston or pino
logger.info("user_login", { userId, ip: req.ip, timestamp: Date.now() });The structured version attaches a severity level, a machine-readable event name, and consistent fields, making it possible to filter and search logs later rather than parsing free-form text.
Logging at different severity levels
logger.debug("cache lookup", { key: cacheKey }); // routine, verbose detail
logger.info("order created", { orderId }); // normal operation
logger.warn("payment retry", { orderId, attempt: 2 }); // recoverable issue
logger.error("payment failed", { orderId, err }); // needs attentionSeverity levels let you dial the verbosity up or down per environment — full detail in development, only warnings and errors in production — without changing the logging calls themselves.
How it works
A logging library gives you methods for each severity level (debug, info, warn, error) instead of one flat console.log. Each call records a structured entry — typically including a timestamp, the level, a message, and any extra data you attach — and sends it to one or more destinations: the terminal during development, and often a file or an external logging service in production, where entries from many server instances can be searched and monitored together. A configured minimum level (like "only info and above") controls which calls actually get recorded in a given environment.
Why does it exist?
Production problems are almost never diagnosed live — they're investigated after the fact, often much later, by someone who wasn't watching the server when the problem happened. Logging exists to leave behind a durable, searchable trail of what the system was doing, so that trail can answer questions no one thought to ask in the moment.
When to use it
Log meaningful events: requests, errors, retries, and any decision point useful for understanding behavior later — especially around payments, authentication, and anything involving external services that can fail.
When not to use it
Don't log extremely high-frequency, low-value events at a verbose level in production (logging every single cache hit, for instance) — it drowns out the signal you actually need and can itself become a performance or cost problem.
Common mistakes
Logging sensitive data — passwords, full credit card numbers, tokens — directly into logs where they can be seen or leaked.
Using one severity level (usually everything as
console.log) for everything, making it impossible to filter noise from real problems.Relying only on logs that live in a terminal or a single server's disk, which disappear when that specific process or machine goes away.
Practice exercises
- Easy:
Replace three
console.logcalls in a small Express app with structuredlogger.infocalls that include relevant context data. - Medium:
Add a
logger.errorcall inside a catch block that records the error and the request path, without leaking the raw error to the client's response. - Hard:
Explain why logging a user's raw password would be a serious mistake even if the log file itself is 'internal only,' and describe a safer alternative.
Interview questions
Why is structured logging preferred over ad-hoc console.log statements in production?
Structured logs are timestamped, leveled, and machine-readable, making them searchable and filterable after the fact, and persist beyond a single terminal session.
What are severity levels in logging, and why do they matter?
Levels like debug, info, warn, and error indicate how important a log entry is, letting you control verbosity per environment — for example, showing full detail in development but only warnings and errors in production.
What kind of data should never be written to logs?
Sensitive information such as passwords, full payment card numbers, or authentication tokens, since logs can be read by more people or systems than the original data was meant for.
Why can excessive logging itself become a performance problem in production?
Every log call has a cost — serializing the data and writing it to disk or over the network — and at high request volumes, logging every routine event at a verbose level can add meaningful latency or consume significant storage budget, competing with the actual work the server is meant to do.
What does it mean to set a minimum log level in production, e.g. only warn and above?
Any log call below that threshold, like debug or info, is suppressed entirely and never written anywhere, letting the same logging calls in the code produce quiet output in production but full detail in development, just by changing one configuration value rather than editing the code.
Why is including a request or correlation ID in every log line tied to a single incoming request useful?
Under concurrent traffic, log lines from many simultaneous requests interleave in the same output stream; a shared id attached to every log line generated while handling one specific request lets you filter and reconstruct that single request's full story afterward, even mixed in with thousands of others.
Why do production systems typically send logs to a centralized aggregation service instead of just writing to a local file?
It collects logs from every server instance into one searchable place — a log written only to one instance's local disk is lost if that instance is replaced or scaled down, and impossible to search across instances without a central destination.
Why might a team choose JSON as the on-the-wire format for log entries rather than a human-readable sentence?
JSON can be parsed reliably by log-aggregation tooling — fields can be filtered, aggregated, and queried directly — whereas a free-form sentence has to be pattern-matched or regex-parsed, which is brittle and loses structure the moment the wording changes slightly.
What's the risk of logging an entire request or response body indiscriminately?
Request and response bodies often contain exactly the sensitive data that shouldn't be logged, and can also be large, so logging them wholesale both creates a security or privacy liability and bloats log storage with mostly redundant detail.
Why should error logs typically include the full stack trace, not just the error's message string?
The message alone often says what went wrong but not where — the stack trace shows the exact call chain leading to the failure, which is usually what's actually needed to locate and fix the bug.
What's the difference between logging and monitoring or alerting?
Logging records discrete events for later inspection; monitoring and alerting watch metrics or log patterns continuously and proactively notify someone when something crosses a threshold — logs are often the raw material an alerting system watches, but the two serve different immediate purposes.
Why is `console.log` particularly unsuitable for a production Node.js server handling concurrent requests?
It writes synchronously to standard output by default, which can block the event loop under heavy load, and produces unstructured, untimestamped, unleveled text that isn't tied to any deployment's aggregation or filtering tooling.
Why might you log at the `warn` level for a recoverable failure, like a successful retry after one failed attempt, instead of `error`?
Reserving error for events that need actual attention keeps that level meaningful and actionable; logging every merely-recovered hiccup as error too would create alert fatigue and bury genuinely urgent errors in noise.
How would you redact a sensitive field, like a password or token, from a log entry while still logging the rest of the object it's part of?
Explicitly replace or omit just that field before logging, e.g. destructuring it out or masking it, rather than logging the raw object as-is and hoping nothing sensitive is inside it.
Why might logging configuration differ between local development and production, beyond just the minimum level?
Development often wants colorized, human-readable console output for a person actively reading it; production usually wants machine-parseable JSON destined for an aggregation service, and possibly different destinations entirely — the logging calls in the code stay the same, only the configured output format and destinations change per environment.