console.log writes a sentence. A log aggregator wants a record: fields it can filter, sort and alert on. Pino 10.3.1 18,227 (github.com/pinojs/pino (https://github.com/pinojs/pino 18,227 ), npm 2,036 i pino, MIT) writes one JSON object per line to stdout, and it is fast because it concatenates each line against a schema compiled when the logger is created rather than calling JSON.stringify on an arbitrary object.
import pino from 'pino';
const log = pino({ base: null });
log.info('service started');
log.info({ orderId: 'A-1042', total: 78.5 }, 'order placed');
const req = log.child({ reqId: '9f2c' });
req.warn({ retryIn: 250 }, 'gateway slow');
log.debug('not printed at the default level');{"level":30,"time":1789645187551,"msg":"service started"}
{"level":30,"time":1789645187552,"orderId":"A-1042","total":78.5,"msg":"order placed"}
{"level":40,"time":1789645187552,"reqId":"9f2c","retryIn":250,"msg":"gateway slow"}Every method takes an optional object first and the message second — log.info(fields, msg), never the other way round. log.child(fields) merges those fields into every line for almost nothing, the mechanism behind per-request logging in Request Logging. The debug call printed nothing because the default level is info, and base: null drops the pid and hostname pino adds by default.
JSON is unreadable at a terminal, so development pipes it through pino-pretty 13.1.3 (npm i -D pino-pretty, MIT) — node logger.mjs | npx pino-pretty — which reformats each line as it arrives. Never in production: it is a second process parsing every line, and pretty text defeats the aggregator.
[19:39:47.731] INFO: order placed
orderId: "A-1042"
total: 78.5
[19:39:47.731] WARN: gateway slow
reqId: "9f2c"
retryIn: 250Know the alternatives. winston 3.19.0 24,519 (MIT) is more flexible — pluggable formats, several in-process transports — and pays for it per line; bunyan 1.8.15 (MIT) pioneered JSON logging in Node but is barely maintained. All three beat console.log, which has no levels, no fields and no redaction.