bunyan
Bunyan is a CommonJS logging library for Node.js services. Every call writes one JSON record with standard fields such as name, hostname, process ID, level, message, and time, which makes logs easy for machines to ingest. It also includes a command-line formatter for humans, child loggers that bind context, serializers for errors and HTTP objects, and a stream system for sending different severity levels to different destinations.
Keep Bunyan where its record format and tooling are already part of the system. For a new Node.js service, Pino offers a more current path with less maintenance uncertainty and better performance priorities.
Use it if
- You maintain an existing Node.js service whose log pipeline, dashboards, or conventions already expect Bunyan records
- You want newline-delimited JSON on stdout plus a bundled CLI that can pretty-print and filter the same records
- You need child loggers to bind request IDs or component names without passing those fields at every call
- You need multiple Node writable streams with separate severity thresholds or a raw stream that receives record objects
- You are choosing a logger for a new service: npm still installs 1.8.15 from January 2021, while the README has described 2.x as beta for years and the repository was last pushed in September 2023
- You want an ESM-first or TypeScript-first package: Bunyan 1.x is CommonJS and ships no declarations, so TypeScript projects need the separate @types/bunyan package
- You need fast production logging under heavy load: Bunyan builds and stringifies records synchronously, while Pino is designed around lower-overhead JSON logging
- You run clustered workers that write one rotating file: the README explicitly warns that multiple processes sharing a rotating-file path can rotate it unpredictably
- You need rotation by file size or custom rotated filenames: Bunyan's built-in rotating-file stream supports time periods and numbered backups, but neither size thresholds nor filename templates
Setup reality
Installing `bunyan` is simple, but its age shows around the edges. Version 1.8.15 has four optional dependencies: `dtrace-provider` enables DTrace, `mv` supports the rotating-file stream, `moment` supports local-time output in the CLI, and `safe-json-stringify` protects record serialization. npm may therefore attempt platform-sensitive optional installation work even if your app only logs JSON to stdout; omit optional dependencies if those features are irrelevant, then do not configure features that rely on them. TypeScript definitions are not bundled, so add `@types/bunyan` separately. A logger requires a `name`, defaults to stdout at info level, and writes raw JSON rather than pretty terminal text. Pipe development output through `bunyan`, but keep raw JSON in production for collectors. Error, request, and response objects only get useful shapes when you configure `bunyan.stdSerializers`; the request and response serializers intentionally omit bodies. Do not enable `src: true` in production because the README calls source-location discovery slow. File rotation also needs operational care: clustered processes cannot safely share one rotating path, size-based rotation is unavailable, and external rotation without copy-truncate requires your process to call `reopenFileStreams()` after the file descriptor changes. Stream errors are EventEmitter errors, not error-level log records, so attach a logger error handler when file writes can fail.
Patterns
Create a JSON loggercreate-logger
const bunyan = require('bunyan');
const log = bunyan.createLogger({ name: 'orders-api' });
log.info('service started');The name is required. With no stream configuration, Bunyan writes JSON to stdout at info level.
Add searchable fields to a recordlog-structured-fields
log.info(
{ orderId: 'ord_123', customerId: 'cus_9', totalCents: 2499 },
'order accepted'
);Put application objects under named fields. Passing an arbitrary object directly can collide with Bunyan's core fields.
Preserve error message and stacklog-errors
const log = bunyan.createLogger({
name: 'worker',
serializers: bunyan.stdSerializers,
});
try {
await runJob();
} catch (err) {
log.error({ err, jobId: 'job_42' }, 'job failed');
}Configure the err serializer when an Error shares a record with other fields; otherwise the useful non-enumerable Error properties can disappear.
Create a child logger for one requestbind-request-context
function handleRequest(req, res) {
const reqLog = log.child({ reqId: req.headers['x-request-id'] });
reqLog.info({ path: req.url }, 'request started');
// Every later reqLog call includes reqId.
}Child fields are copied onto every record. Avoid the optional simple-child shortcut unless profiling proves you need it, because it skips safety checks.
Serialize Node HTTP objectsserialize-http-request
const httpLog = bunyan.createLogger({
name: 'http',
serializers: bunyan.stdSerializers,
});
httpLog.info({ req }, 'request received');
httpLog.info({ res }, 'response sent');The standard req and res serializers intentionally omit bodies, which avoids accidental huge payloads but does not by itself redact sensitive headers.
Send errors to a second destinationroute-by-level
const log = bunyan.createLogger({
name: 'payments',
streams: [
{ level: 'info', stream: process.stdout },
{ level: 'error', path: '/var/log/payments-error.log' },
],
});An error record goes to both streams because each level is a minimum threshold. File-stream errors are re-emitted by the logger.
Handle file stream failureshandle-stream-errors
log.on('error', (err, stream) => {
process.stderr.write(`logging failed for ${stream.path || 'stream'}: ${err.message}\n`);
});A stream error event is separate from log.error(). Without an error listener, an EventEmitter error can terminate the process.
Raise or lower verbosity at runtimechange-log-level
log.level('debug');
if (log.debug()) {
log.debug({ state: buildExpensiveDebugState() }, 'worker state');
}Calling a level method with no arguments checks whether that level is enabled, so expensive debug data need not be built when filtered out.
Pretty-print and filter JSON logspretty-print-cli
node server.js | npx bunyan
node server.js | npx bunyan -l warn
npx bunyan app.log -c 'this.reqId == "req_123"'Keep production output as JSON. The CLI is a viewing and filtering layer, and its condition expression processes log content as JavaScript.
Rotate a file on a time schedulerotate-log-files
const log = bunyan.createLogger({
name: 'batch',
streams: [{
type: 'rotating-file',
path: '/var/log/batch.log',
period: '1d',
count: 7,
}],
});Rotation is time-based only. Do not point multiple cluster workers at the same path; use separate paths or an external log service.
Reopen files after external rotationreopen-external-log
process.on('SIGUSR2', () => {
log.reopenFileStreams();
});Use this when logrotate renames the file without copytruncate; otherwise Bunyan keeps writing through the old file descriptor.
Keep recent trace records in memorybuffer-recent-records
const ring = new bunyan.RingBuffer({ limit: 100 });
const log = bunyan.createLogger({
name: 'worker',
streams: [
{ level: 'info', stream: process.stdout },
{ level: 'trace', type: 'raw', stream: ring },
],
});
console.log(ring.records);A raw stream receives record objects rather than JSON strings. Bound the ring carefully because every retained record consumes process memory.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pino | npm | New Node services that want fast structured JSON logging, transports, and current maintenance |
| winston | npm | Applications that value a large transport ecosystem and configurable formats over a fixed JSON record shape |
| log4js | npm | Teams that want log4j-style categories, appenders, and configuration-driven routing in Node.js |