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.
On this page · 5 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
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#
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#
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');
}
{"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#
- The debug line is dropped because the level is info.
- Each line is one JSON object with a numeric level (30 info, 40 warn, 50 error) and the base field service.
- The child logger adds requestId to every line without repeating it.
- redact replaces the password and the Authorization header.
- 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#
Request bodies and headers contain passwords and tokens. Redact or do not log them.
"order " + id + " failed" cannot be filtered by orderId. Put values in fields.
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?
Show solution
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#
Why prefer JSON logs over plain text?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.