Node.js · APIs & Data

Logging

When something breaks in production, logs are your eyes. Structured, levelled logs with request ids let you find the one failing request among millions — without leaking passwords.

Advanced 5 min Lesson 32 of 37Examples target Node.js 22 LTS.
Before you start:Error Handling
0/37 in Node.js
On this page · 5 min · Advanced

Learning objectives#

  • Use log levels consistently
  • Write structured JSON logs with Pino
  • Add request ids with child loggers
  • Redact sensitive fields

Explanation#

console.log writes plain text with no level, no timestamp and no structure — hard to search and filter at scale.

Pino (npm install pino) is a fast logger that writes one JSON object per line: {"level":30,"time":...,"msg":"order placed","orderId":1001}. Log platforms (Loki, Elasticsearch, CloudWatch, Azure Monitor) index each field.

Levels: trace, debug, info (normal events), warn (unexpected but handled), error (failed operation), fatal. Set LOG_LEVEL per environment (debug locally, info in production).

Child loggers add context to every line: const log = logger.child({ requestId }); then log.info("...") includes the requestId automatically.

Redact secrets: pino({ redact: ["req.headers.authorization", "*.password"] }) replaces them with [Redacted]. Never log passwords, tokens or card numbers.

Syntax#

JavaScript Illustrative
import pino from 'pino';
const logger = pino({ level: process.env.LOG_LEVEL ?? 'info', redact: ['*.password'] });
logger.info({ orderId: 1001, total: 499 }, 'order placed');
const log = logger.child({ requestId: 'a1b2' });
log.warn('stock low');

Code example: Structured logs with levels, context and redaction#

JavaScriptlogging.js Illustrative
import pino from 'pino';


const logger = pino({
  level: 'info',
  base: { service: 'shop-api' },                       // added to every line
  timestamp: false,                                    // removed only to keep this demo's output stable
  redact: { paths: ['user.password', 'headers.authorization'], censor: '[REDACTED]' },
});


logger.debug('cache warm-up details');                 // below "info": not written
logger.info({ port: 3000 }, 'server started');


// Per-request child logger
const log = logger.child({ requestId: 'req-7f3a' });
log.info({ user: { email: 'asha@example.com', password: 'p@ssword1' } }, 'login attempt');
log.warn({ productId: 3, stock: 1 }, 'stock low');


try {
  JSON.parse('{oops');
} catch (err) {
  log.error({ error: { type: err.name, message: err.message }, headers: { authorization: 'Bearer abc.def' } }, 'failed to parse body');
}
Expected output
{"level":30,"service":"shop-api","port":3000,"msg":"server started"}
{"level":30,"service":"shop-api","requestId":"req-7f3a","user":{"email":"asha@example.com","password":"[REDACTED]"},"msg":"login attempt"}
{"level":40,"service":"shop-api","requestId":"req-7f3a","productId":3,"stock":1,"msg":"stock low"}
{"level":50,"service":"shop-api","requestId":"req-7f3a","error":{"type":"SyntaxError","message":"Expected property name or '}' in JSON at position 1 (line 1 column 2)"},"headers":{"authorization":"[REDACTED]"},"msg":"failed to parse body"}

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The debug line is dropped because the level is info.
  2. Each line is one JSON object with a numeric level (30 info, 40 warn, 50 error) and the base field service.
  3. The child logger adds requestId to every line without repeating it.
  4. redact replaces the password and the Authorization header.
  5. In production you would keep timestamps (removed here only for a stable output).

Real-world use case#

A support ticket says “checkout failed at 14:32”. Searching the log platform for level>=50 and the customer’s requestId shows the exact error, the order id and the payment provider’s response in seconds.

Common mistakes#

Logging secrets

Request bodies and headers contain passwords and tokens. Redact or do not log them.

String concatenation

"order " + id + " failed" cannot be filtered by orderId. Put values in fields.

Everything at info or error

Wrong levels make alerts noisy or useless. Use warn and debug deliberately.

Best practices#

  • JSON logs with a fast logger (Pino)
  • Level from environment
  • Request ids via child loggers
  • Redact sensitive fields

Remember this#

One JSON object per line, values in fields.

debug < info < warn < error < fatal.

Child logger = automatic context (requestId).

Practice exercise#

Your task: Create a Pino logger with level "warn" and log one info and one warn message. Which one is written?

Need a hint?
Only levels at or above the configured level are written.
Show solution
JavaScript Illustrative
import pino from 'pino';


const logger = pino({ level: 'warn', timestamp: false, base: undefined });
logger.info('not written');
logger.warn({ disk: '92%' }, 'disk almost full');
// {"level":40,"disk":"92%","msg":"disk almost full"}

Mini quiz#

Logging quizMultiple choice Question 1 of 3

Why prefer JSON logs over plain text?

Score so far: 0/0

Want more? Practise all Node.js MCQs · Node.js interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.