bunyan review
Bunyan 1.8.15 turns each Node.js log call into one JSON line with fixed fields for the logger name, host, process, level, message, timestamp, and record version. The package also supplies a terminal formatter, child loggers for bound context, serializers for Error and HTTP objects, and stream routing by level. The current release teaches the request serializer to read Express `originalUrl`; its other stated change replaces the obsolete nodeunit test runner with node-tap 9. This remains a CommonJS, Node-focused logger rather than a browser logging package.
Bunyan 1.8.15 took 9.4 seconds and 8 MB in our sandbox, emitted 3 deprecation warnings, and failed the browser bundle, so keep it for established Node log pipelines rather than choosing it for a new project. Its JSON record format and CLI still do their jobs, but the January 2021 latest release and missing bundled types are real costs.
We installed it
| Install | ✓ · 9.4s | 21 packages on disk · 8 MB · 3 deprecation warnings |
| Import | ✓ | ESM import works · require() works · CommonJS package |
| Browser | n/a | could not be bundled for the browser (Node-only code, most likely) |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does bunyan install cleanly?
Yes. In a fresh container with an empty cache, npm install bunyan finished in 9 seconds, leaving 21 packages and 8 MB on disk. npm audit reported no known vulnerabilities. The install printed 3 deprecation warnings.
Can bunyan run in a browser?
Not directly: esbuild could not bundle it for the browser in our run, which normally means it depends on Node built-ins. Use it on the server, or find a browser-targeted alternative.
Does bunyan work with both ESM and CommonJS?
Yes. Both import 'bunyan' and require('bunyan') worked in Node 22 in our run. The package is published as CommonJS.
Does bunyan include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
bunyan or pino: which should you use?
pino: Choose it for a new Node service where low logging overhead and active releases matter. Bunyan 1.8.15 took 9.4 seconds and 8 MB in our sandbox, emitted 3 deprecation warnings, and failed the browser bundle, so keep it for established Node log pipelines rather than choosing it for a new project.
When should you not use bunyan?
You are picking a logger for new Node code: npm's latest tag still points to 1.8.15 from January 2021, while 2.0.5 remains on the beta tag
Use it if
- An existing service or log collector already depends on Bunyan's newline-delimited record shape
- You want raw JSON in production and the matching `bunyan` CLI for local filtering and readable output
- Request IDs, tenant IDs, or component names should be attached once through child loggers
- Different severity levels need to flow to separate Node writable streams
- You are picking a logger for new Node code: npm's latest tag still points to 1.8.15 from January 2021, while 2.0.5 remains on the beta tag
- Your codebase requires package-supplied TypeScript declarations; our 1.8.15 install contained none, so you must rely on `@types/bunyan`
- The same rotating log file will be written by several cluster workers; the README warns that built-in rotation is unsafe in that arrangement
- You need rotation by byte size or custom archive names; Bunyan's rotating stream exposes a time period and numbered backups instead
- The logger must run in a browser build; our esbuild browser bundle failed on Node-only code
Setup reality
Our Bunyan 1.8.15 install completed in 9.4 seconds in a fresh Node 22 container. It left 21 packages using 8 MB on disk and printed 3 deprecation warnings. npm audit reported 0 known vulnerabilities. The installed package itself was 224 KB unpacked, exposed 0 peer dependencies, and our measurement recorded 0 direct dependencies.
Both require('bunyan') and ESM import loaded the CommonJS package, but there is no exports map and no bundled TypeScript declaration. TypeScript users need @types/bunyan. A logger must have a name; without an explicit stream it writes JSON to stdout at info level. Use the CLI only to format or filter that JSON for people.
Configure bunyan.stdSerializers before expecting useful err, req, or res fields. The HTTP serializers leave bodies out, but they do not promise to scrub headers. Version 1.8.15 now reads Express req.originalUrl. Avoid src: true on a hot path because the README calls source discovery slow.
Built-in file rotation is time based and cannot coordinate several processes sharing one path. If an external rotator renames a log file, call reopenFileStreams() after rotation or Bunyan keeps the old descriptor. Stream failures arrive as EventEmitter error events, so file-backed loggers need an error listener. Our browser bundle attempt failed, which confirms this package belongs in Node services.
Patterns
Write JSON records to stdout create-logger
const bunyan = require('bunyan');
const log = bunyan.createLogger({ name: 'orders-api' });
log.info('service started');`name` is mandatory. With no stream list, version 1.8.15 emits info-and-higher JSON records to stdout.
Attach fields without colliding with the record add-fields
log.info({
orderId: 'ord_123',
totalCents: 2499
}, 'order accepted');Put a domain object under a named property. Bunyan merges the first object into the record, so arbitrary keys can overwrite reserved fields.
Serialize an Error with context log-error
const log = bunyan.createLogger({
name: 'worker',
serializers: bunyan.stdSerializers
});
try {
await runJob();
} catch (err) {
log.error({ err, jobId: 'job_42' }, 'job failed');
}`stdSerializers.err` preserves the message, name, stack, and code that ordinary JSON serialization can lose from an Error.
Bind a request ID to a child child-context
function requestLogger(requestId) {
return log.child({ requestId });
}
requestLogger('req_7').info({ path: '/orders' }, 'received');Every child call includes `requestId`. Child bindings are a better fit than mutating a shared logger during concurrent requests.
Shape Node request and response objects http-serializers
const httpLog = bunyan.createLogger({
name: 'http',
serializers: bunyan.stdSerializers
});
httpLog.info({ req }, 'request received');
httpLog.info({ res }, 'response sent');Version 1.8.15 reads Express `originalUrl`. The standard serializers omit bodies but can still include header data that your policy may require you to redact.
Route severe records to a file route-levels
const log = bunyan.createLogger({
name: 'payments',
streams: [
{ level: 'info', stream: process.stdout },
{ level: 'error', path: '/var/log/payments-error.log' }
]
});A level is a minimum threshold, so error and fatal records go to both destinations in this 2-stream configuration.
Catch destination failures handle-stream-error
log.on('error', (err, stream) => {
const target = stream.path || 'configured stream';
process.stderr.write(`log write failed for ${target}: ${err.message}\n`);
});A file-stream failure is an EventEmitter error, separate from calling `log.error()`. An unhandled `error` event can stop the process.
Skip expensive debug field creation guard-debug-work
if (log.debug()) {
log.debug({ state: buildDebugState() }, 'worker state');
}Calling a level method with no arguments returns whether that level is enabled, so filtered records do not require building their fields.
Read and filter Bunyan JSON filter-cli
node server.js | npx bunyan
node server.js | npx bunyan -l warn
npx bunyan app.log -c 'this.requestId == "req_7"'The CLI formats existing newline-delimited records. Keep the raw JSON as the source consumed by production collectors.
Retain seven daily log files rotate-by-time
const log = bunyan.createLogger({
name: 'batch',
streams: [{
type: 'rotating-file',
path: '/var/log/batch.log',
period: '1d',
count: 7
}]
});The built-in stream rotates by time, not bytes. Several worker processes must not share this rotating path.
Reopen descriptors after logrotate reopen-file
process.on('SIGUSR2', () => {
log.reopenFileStreams();
});Call this after an external tool renames active files without copytruncate; otherwise Bunyan continues writing to the old descriptor.
Keep the latest trace records in memory ring-buffer
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 JavaScript record objects. The `limit: 100` bound matters because retained records stay in process memory.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pino | npm | Choose it for a new Node service where low logging overhead and active releases matter. |
| winston | npm | Choose it when transports and configurable output formats matter more than Bunyan's fixed record convention. |
| log4js | npm | Choose it for category-based configuration and log4j-style appenders. |
More infra guides
boto3 · opentelemetry-api · psutil · distro · @opentelemetry/api · google-cloud-storage · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

