Bunyan

Node.js logging library focused on structured JSON. Designed based on philosophy that 'logs should be JSON objects, not strings'. Provides excellent CLI tool for beautiful display and filtering, with features specialized for production issue analysis.

Logging LibraryJavaScriptNode.jsJSON-focusedStructured LoggingStreams

GitHub Overview

trentm/node-bunyan

a simple and fast JSON logging module for node.js services

Stars7,200
Watchers114
Forks517
Created:January 30, 2012
Language:JavaScript
License:Other

Topics

None

Star History

trentm/node-bunyan Star History
Data as of: 7/17/2025, 10:01 AM

Library

Bunyan

Overview

Bunyan is a structured JSON-focused logging library for Node.js specializing in JSON format output. Provides structured data, hierarchical loggers, log streaming, and command-line viewer. Excellent for log analysis and observability through JSON-first approach, with all log entries having consistent JSON structure enabling easy machine analysis and processing. Implements flexible output destination support through stream-based architecture, automatic formatting of common objects via serializers, and context inheritance through child loggers.

Details

Bunyan is positioned as important choice for Node.js development prioritizing structured logging in 2025. High affinity with log analysis platforms like ELK Stack and Splunk through JSON-first approach, suitable for distributed log management in microservices. Each log record is one line of JSON.stringify'd output with automatic fields (pid, hostname, time, v) added, and common objects like Error and HTTP requests are processed by dedicated serializers. Rich integration with cloud services like Google Cloud Logging and addresses modern observability requirements.

Key Features

  • JSON-First Design: Easy machine analysis through consistent JSON structure for all log entries
  • Built-in CLI Tool: Command-line tool for log display and filtering during development
  • Serializers: Automatic JSON conversion of common objects like Error and HTTP requests
  • Stream-based: Flexible output destination control through Node.js streams
  • Child Loggers: Field inheritance and scope management through context-specific loggers
  • 6-Level Hierarchy: Hierarchical control with trace, debug, info, warn, error, fatal

Pros and Cons

Pros

  • Extremely easy automated analysis and processing through machine-readable JSON format logs
  • High affinity with log analysis platforms like ELK Stack and Splunk
  • Optimal for distributed log management in microservices through structured data
  • Appropriate automatic formatting of complex objects through serializers
  • Efficient context information management through child logger functionality
  • Standard integration with cloud services like Google Cloud Logging
  • Development-time log display and debugging support through CLI tools

Cons

  • Currently not actively maintained, cannot expect new feature additions
  • Poor readability when humans read directly due to JSON output
  • File sizes tend to be larger than plain text logs
  • Overkill for use cases requiring lightweight logging
  • Some features are inferior compared to Winston and Pino in terms of richness
  • Compatibility issues with existing log formats in legacy projects

References

Code Examples

Installation and Basic Setup

# Install Bunyan
npm install bunyan

# Global install of CLI tool for development (optional)
npm install -g bunyan
// ES6 modules (recommended)
import bunyan from 'bunyan';

// CommonJS
const bunyan = require('bunyan');

// Create simplest logger
const log = bunyan.createLogger({name: 'myapp'});

// Basic log output
log.trace('Trace message');
log.debug('Debug message');
log.info('Info message');
log.warn('Warning message');
log.error('Error message');
log.fatal('Fatal error message');

// Output example (JSON format)
// {"name":"myapp","hostname":"localhost","pid":1234,"level":30,"msg":"Info message","time":"2025-01-01T12:00:00.000Z","v":0}

// Structured data logging
log.info({user_id: 123, action: 'login'}, 'User login');

Basic Logging Operations (Levels, Formatting)

import bunyan from 'bunyan';

// Logger with detailed configuration
const log = bunyan.createLogger({
  name: 'web-service',
  level: 'info',
  src: true,  // Include source file information (recommended for development only)
});

// Level-specific log output
log.trace({component: 'database'}, 'Trace level debug information');
log.debug({sql: 'SELECT * FROM users'}, 'SQL execution debug');
log.info({user_id: 456}, 'User operation log');
log.warn({threshold: 1000, actual: 1500}, 'Response time warning');
log.error({error_code: 'DB_001'}, 'Database error');
log.fatal({system: 'payment'}, 'System fatal error');

// Combination of object and string message
log.info({
  request_id: 'req_123',
  method: 'POST',
  url: '/api/users',
  duration_ms: 150
}, 'Request processing completed');

// Conditional logging (performance consideration)
if (log.trace()) {
  const heavyDebugData = generateExpensiveDebugData();
  log.trace({debug_data: heavyDebugData}, 'Heavy processing debug information');
}

// Dynamic level setting changes
log.level(bunyan.DEBUG);  // Output DEBUG level and above
log.level('warn');        // Output WARN level and above

// Log level checking
console.log('Current log level:', log.level());
console.log('Is DEBUG enabled?', log.debug());
console.log('Is INFO enabled?', log.info());

Advanced Configuration and Customization (Streams, Serializers)

import bunyan from 'bunyan';
import fs from 'fs';

// Multiple stream configuration
const log = bunyan.createLogger({
  name: 'multi-stream-app',
  streams: [
    {
      level: 'debug',
      stream: process.stdout  // Console output
    },
    {
      level: 'info',
      path: './logs/app.log'  // File output
    },
    {
      level: 'error',
      path: './logs/error.log'  // Error-specific file
    }
  ]
});

// Rotating file stream
import RotatingFileStream from 'bunyan-rotating-file-stream';

const rotatingLog = bunyan.createLogger({
  name: 'rotating-app',
  streams: [{
    type: 'raw',
    stream: new RotatingFileStream({
      path: './logs/app-%Y-%m-%d.log',
      period: '1d',        // Daily rotation
      totalFiles: 10,      // Keep maximum 10 files
      rotateExisting: true,
      gzip: true          // Compress old files
    })
  }]
});

// Custom serializers
const log = bunyan.createLogger({
  name: 'custom-serializers',
  serializers: {
    // HTTP request serializer
    req: (req) => ({
      method: req.method,
      url: req.url,
      headers: req.headers,
      remoteAddress: req.connection.remoteAddress,
      remotePort: req.connection.remotePort
    }),
    
    // HTTP response serializer
    res: (res) => ({
      statusCode: res.statusCode,
      headers: res.getHeaders()
    }),
    
    // Custom user object serializer
    user: (user) => ({
      id: user.id,
      username: user.username,
      role: user.role,
      // Exclude sensitive information like passwords
    }),
    
    // Error serializer (extending default)
    err: bunyan.stdSerializers.err
  }
});

// Serializer usage example
log.info({
  req: request,
  res: response,
  user: currentUser
}, 'API call completed');

// Custom stream (example: Slack notification)
class SlackStream {
  constructor(webhookUrl) {
    this.webhookUrl = webhookUrl;
  }
  
  write(record) {
    // Send to Slack only for error level and above
    if (record.level >= bunyan.ERROR) {
      this.sendToSlack({
        text: `🚨 ${record.name}: ${record.msg}`,
        attachments: [{
          color: 'danger',
          fields: [
            { title: 'Level', value: bunyan.nameFromLevel[record.level], short: true },
            { title: 'Time', value: record.time, short: true },
            { title: 'Host', value: record.hostname, short: true },
            { title: 'PID', value: record.pid, short: true }
          ]
        }]
      });
    }
  }
  
  async sendToSlack(payload) {
    try {
      await fetch(this.webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });
    } catch (error) {
      console.error('Slack notification error:', error);
    }
  }
}

// Slack stream usage example
const slackLog = bunyan.createLogger({
  name: 'slack-notifier',
  streams: [
    { stream: process.stdout },
    { 
      type: 'raw',
      stream: new SlackStream('https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK')
    }
  ]
});

Structured Logging and Modern Observability Support

import bunyan from 'bunyan';
import { v4 as uuidv4 } from 'uuid';

// Distributed tracing logger
class TracingLogger {
  constructor(name, options = {}) {
    this.log = bunyan.createLogger({
      name,
      serializers: {
        ...bunyan.stdSerializers,
        trace: (trace) => ({
          trace_id: trace.traceId,
          span_id: trace.spanId,
          parent_span_id: trace.parentSpanId,
          service_name: trace.serviceName,
          operation_name: trace.operationName
        })
      },
      ...options
    });
  }
  
  withTrace(traceInfo = {}) {
    const traceId = traceInfo.traceId || uuidv4();
    const spanId = traceInfo.spanId || uuidv4();
    
    return this.log.child({
      trace: {
        traceId,
        spanId,
        parentSpanId: traceInfo.parentSpanId,
        serviceName: traceInfo.serviceName || 'unknown-service',
        operationName: traceInfo.operationName
      }
    });
  }
}

// Usage example
const tracingLogger = new TracingLogger('order-service');

const orderLogger = tracingLogger.withTrace({
  serviceName: 'order-service',
  operationName: 'process_order'
});

orderLogger.info({
  order_id: 'order_123',
  user_id: 456,
  amount: 99.99,
  currency: 'USD'
}, 'Order processing started');

// OpenTelemetry-style logging
function logSpan(logger, spanName, operation) {
  const startTime = Date.now();
  const spanId = uuidv4();
  
  logger.info({
    span: {
      name: spanName,
      span_id: spanId,
      start_time: startTime,
      kind: 'internal'
    }
  }, `Span started: ${spanName}`);
  
  try {
    const result = operation();
    const duration = Date.now() - startTime;
    
    logger.info({
      span: {
        name: spanName,
        span_id: spanId,
        duration_ms: duration,
        status: 'ok'
      }
    }, `Span completed: ${spanName}`);
    
    return result;
  } catch (error) {
    const duration = Date.now() - startTime;
    
    logger.error({
      span: {
        name: spanName,
        span_id: spanId,
        duration_ms: duration,
        status: 'error'
      },
      err: error
    }, `Span failed: ${spanName}`);
    
    throw error;
  }
}

// Metrics integrated logger
const metricsLogger = bunyan.createLogger({
  name: 'metrics-logger',
  serializers: {
    metric: (metric) => ({
      name: metric.name,
      value: metric.value,
      type: metric.type,
      labels: metric.labels,
      timestamp: metric.timestamp || Date.now()
    })
  }
});

function recordMetric(name, value, type = 'gauge', labels = {}) {
  metricsLogger.info({
    metric: {
      name,
      value,
      type,
      labels
    }
  }, `Metric recorded: ${name}`);
}

// Business metrics recording
recordMetric('orders.completed', 1, 'counter', {
  payment_method: 'credit_card',
  customer_tier: 'premium'
});

recordMetric('response.time', 150, 'histogram', {
  endpoint: '/api/orders',
  method: 'POST'
});

// Structured event logging
const eventLogger = bunyan.createLogger({
  name: 'event-logger',
  serializers: {
    event: (event) => ({
      type: event.type,
      category: event.category,
      action: event.action,
      timestamp: event.timestamp || new Date().toISOString(),
      actor: event.actor,
      target: event.target,
      context: event.context
    })
  }
});

// Security events
eventLogger.warn({
  event: {
    type: 'security',
    category: 'authentication',
    action: 'failed_login',
    actor: { user_id: null, ip: '192.168.1.100' },
    target: { resource: 'user_account', username: 'admin' },
    context: { 
      user_agent: 'curl/7.68.0',
      attempt_count: 3,
      source: 'external'
    }
  }
}, 'Failed login attempt detected');

// User action events
eventLogger.info({
  event: {
    type: 'user_action',
    category: 'commerce',
    action: 'purchase_completed',
    actor: { user_id: 123, session_id: 'sess_456' },
    target: { resource: 'product', product_id: 'prod_789' },
    context: {
      amount: 99.99,
      payment_method: 'stripe',
      campaign_id: 'summer_sale'
    }
  }
}, 'Purchase completed successfully');

Error Handling and Performance Optimization

import bunyan from 'bunyan';

// Performance optimized logger
class PerformanceOptimizedLogger {
  constructor(name, options = {}) {
    this.batchSize = options.batchSize || 100;
    this.flushInterval = options.flushInterval || 5000;
    this.logBuffer = [];
    this.lastFlush = Date.now();
    
    this.logger = bunyan.createLogger({
      name,
      ...options
    });
    
    // Periodic flushing
    this.flushTimer = setInterval(() => {
      this.flush();
    }, this.flushInterval);
  }
  
  log(level, data, message) {
    this.logBuffer.push({ level, data, message, timestamp: Date.now() });
    
    if (this.logBuffer.length >= this.batchSize) {
      this.flush();
    }
  }
  
  flush() {
    if (this.logBuffer.length === 0) return;
    
    const logs = this.logBuffer.splice(0);
    
    for (const logEntry of logs) {
      this.logger[logEntry.level](logEntry.data, logEntry.message);
    }
    
    this.lastFlush = Date.now();
  }
  
  // Convenience methods
  info(data, message) { this.log('info', data, message); }
  error(data, message) { this.log('error', data, message); }
  warn(data, message) { this.log('warn', data, message); }
  debug(data, message) { this.log('debug', data, message); }
  
  destroy() {
    this.flush();
    clearInterval(this.flushTimer);
  }
}

// Enhanced error handling logger
class RobustLogger {
  constructor(name, options = {}) {
    this.primaryLogger = bunyan.createLogger({ name, ...options });
    this.fallbackLogger = bunyan.createLogger({
      name: `${name}-fallback`,
      stream: process.stderr
    });
    this.errorCount = 0;
    this.maxErrors = 10;
  }
  
  log(level, data, message) {
    try {
      this.primaryLogger[level](data, message);
      this.errorCount = 0; // Reset error count on success
    } catch (error) {
      this.errorCount++;
      
      if (this.errorCount <= this.maxErrors) {
        this.fallbackLogger.error({
          original_log: { level, data, message },
          error: error.message
        }, 'Primary logger failed');
      }
    }
  }
  
  info(data, message) { this.log('info', data, message); }
  error(data, message) { this.log('error', data, message); }
  warn(data, message) { this.log('warn', data, message); }
  debug(data, message) { this.log('debug', data, message); }
}

// Asynchronous log processing
class AsyncLogger {
  constructor(name, options = {}) {
    this.logger = bunyan.createLogger({ name, ...options });
    this.logQueue = [];
    this.processing = false;
  }
  
  async log(level, data, message) {
    return new Promise((resolve, reject) => {
      this.logQueue.push({
        level,
        data,
        message,
        resolve,
        reject
      });
      
      this.processQueue();
    });
  }
  
  async processQueue() {
    if (this.processing || this.logQueue.length === 0) return;
    
    this.processing = true;
    
    while (this.logQueue.length > 0) {
      const logEntry = this.logQueue.shift();
      
      try {
        await new Promise((resolve) => {
          this.logger[logEntry.level](logEntry.data, logEntry.message);
          // Resolve on next tick (non-blocking)
          process.nextTick(resolve);
        });
        
        logEntry.resolve();
      } catch (error) {
        logEntry.reject(error);
      }
    }
    
    this.processing = false;
  }
  
  // Promise-based convenience methods
  async info(data, message) { return this.log('info', data, message); }
  async error(data, message) { return this.log('error', data, message); }
  async warn(data, message) { return this.log('warn', data, message); }
  async debug(data, message) { return this.log('debug', data, message); }
}

// Performance measurement logger
function createPerformanceLogger(name) {
  const logger = bunyan.createLogger({ name });
  
  return {
    measure: async (operationName, operation, context = {}) => {
      const startTime = Date.now();
      const startMemory = process.memoryUsage();
      
      logger.debug({
        operation: operationName,
        ...context
      }, `Operation started: ${operationName}`);
      
      try {
        const result = await operation();
        const duration = Date.now() - startTime;
        const endMemory = process.memoryUsage();
        
        logger.info({
          operation: operationName,
          duration_ms: duration,
          memory_delta: {
            rss: endMemory.rss - startMemory.rss,
            heapUsed: endMemory.heapUsed - startMemory.heapUsed
          },
          status: 'success',
          ...context
        }, `Operation completed: ${operationName}`);
        
        return result;
      } catch (error) {
        const duration = Date.now() - startTime;
        
        logger.error({
          operation: operationName,
          duration_ms: duration,
          status: 'error',
          err: error,
          ...context
        }, `Operation failed: ${operationName}`);
        
        throw error;
      }
    }
  };
}

// Usage examples
const perfLogger = createPerformanceLogger('performance-test');
const robustLogger = new RobustLogger('robust-test');
const asyncLogger = new AsyncLogger('async-test');

// Performance measurement
const result = await perfLogger.measure('database_query', async () => {
  // Database processing simulation
  await new Promise(resolve => setTimeout(resolve, 100));
  return { data: 'query_result' };
}, { user_id: 123 });

// Asynchronous logging
await asyncLogger.info({ user_id: 456 }, 'Async log test');

// Robust logging
robustLogger.error({ error_code: 'SYS_001' }, 'System error occurred');

Framework Integration and Practical Examples

// Express.js integration example
import express from 'express';
import bunyan from 'bunyan';

const app = express();

// Logger for request logging
const requestLogger = bunyan.createLogger({
  name: 'express-app',
  serializers: {
    req: bunyan.stdSerializers.req,
    res: bunyan.stdSerializers.res
  }
});

// Request logging middleware
app.use((req, res, next) => {
  req.log = requestLogger.child({
    request_id: req.headers['x-request-id'] || Date.now().toString()
  });
  
  req.log.info({ req }, 'Request started');
  
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    req.log.info({
      res,
      duration_ms: duration
    }, 'Request completed');
  });
  
  next();
});

// Error handling middleware
app.use((error, req, res, next) => {
  req.log.error({
    err: error,
    req,
    res
  }, 'Request error');
  
  res.status(500).json({ error: 'Internal Server Error' });
});

// Route handler example
app.get('/users/:id', async (req, res) => {
  const userId = req.params.id;
  
  req.log.debug({ user_id: userId }, 'Fetching user');
  
  try {
    const user = await getUserById(userId);
    
    req.log.info({ user_id: userId }, 'User found');
    res.json(user);
  } catch (error) {
    req.log.error({
      err: error,
      user_id: userId
    }, 'Failed to fetch user');
    
    res.status(404).json({ error: 'User not found' });
  }
});

// Hapi.js integration example
import Hapi from '@hapi/hapi';

const server = Hapi.server({
  port: 3000,
  host: 'localhost'
});

const hapiLogger = bunyan.createLogger({
  name: 'hapi-server'
});

// Logger integration as plugin
const loggerPlugin = {
  name: 'logger',
  register: async (server, options) => {
    server.ext('onRequest', (request, h) => {
      request.logger = hapiLogger.child({
        request_id: request.headers['x-request-id'] || request.info.id
      });
      
      request.logger.info({
        method: request.method,
        path: request.path,
        query: request.query
      }, 'Request received');
      
      return h.continue;
    });
    
    server.ext('onPreResponse', (request, h) => {
      const response = request.response;
      
      if (response.isBoom) {
        request.logger.error({
          statusCode: response.output.statusCode,
          error: response.message
        }, 'Request error');
      } else {
        request.logger.info({
          statusCode: response.statusCode
        }, 'Request completed');
      }
      
      return h.continue;
    });
  }
};

await server.register(loggerPlugin);

// Koa.js integration example
import Koa from 'koa';

const koaApp = new Koa();

const koaLogger = bunyan.createLogger({
  name: 'koa-app'
});

koaApp.use(async (ctx, next) => {
  const start = Date.now();
  
  ctx.logger = koaLogger.child({
    request_id: ctx.get('x-request-id') || Date.now().toString()
  });
  
  ctx.logger.info({
    method: ctx.method,
    url: ctx.url,
    user_agent: ctx.get('user-agent')
  }, 'Request started');
  
  try {
    await next();
    
    const duration = Date.now() - start;
    
    ctx.logger.info({
      status: ctx.status,
      duration_ms: duration
    }, 'Request completed');
  } catch (error) {
    const duration = Date.now() - start;
    
    ctx.logger.error({
      err: error,
      status: ctx.status,
      duration_ms: duration
    }, 'Request failed');
    
    throw error;
  }
});

// Worker/Queue integration example
import Queue from 'bull';

const workQueue = new Queue('work queue');

const workerLogger = bunyan.createLogger({
  name: 'queue-worker'
});

workQueue.process(async (job) => {
  const jobLogger = workerLogger.child({
    job_id: job.id,
    job_type: job.data.type
  });
  
  jobLogger.info({ job_data: job.data }, 'Job started');
  
  try {
    const result = await processJob(job.data);
    
    jobLogger.info({
      result,
      duration_ms: Date.now() - job.timestamp
    }, 'Job completed');
    
    return result;
  } catch (error) {
    jobLogger.error({
      err: error,
      job_data: job.data,
      duration_ms: Date.now() - job.timestamp
    }, 'Job failed');
    
    throw error;
  }
});

async function processJob(data) {
  // Job processing logic
  return { status: 'completed', processed_at: new Date() };
}