# Logger Comparison: Winston vs Pino vs Bunyan

# Logger Comparison: Winston vs Pino vs Bunyan

## Problem

Choosing the right logging library for Node.js applications is critical. Developers face multiple options—Winston, Pino, and Bunyan—each with different performance characteristics, features, and use cases. Poor logging choices can impact application performance, especially under high load, while inadequate features can hinder debugging and monitoring.

## Solution

This guide compares three popular Node.js loggers across performance, features, and practical implementation, helping you make an informed decision based on your specific needs.

---

## Logger Overview

### Winston
- **Focus**: Feature-rich, flexible, widely adopted
- **Best for**: Complex logging scenarios, multiple transports
- **Performance**: Moderate (synchronous by default)

### Pino
- **Focus**: Ultra-fast, low-overhead, JSON-first
- **Best for**: High-performance applications, microservices
- **Performance**: Fastest (asynchronous, minimal overhead)

### Bunyan
- **Focus**: JSON logging, structured data, debugging
- **Best for**: Structured logging, log analysis
- **Performance**: Moderate (synchronous, heavier than Pino)

---

## Performance Benchmarks

```javascript
// benchmark.js
const winston = require('winston');
const pino = require('pino');
const bunyan = require('bunyan');

// Winston Configuration
const winstonLogger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'winston.log' })
  ]
});

// Pino Configuration
const pinoLogger = pino({
  level: 'info',
  transport: {
    target: 'pino/file',
    options: { destination: 'pino.log' }
  }
});

// Bunyan Configuration
const bunyanLogger = bunyan.createLogger({
  name: 'benchmark',
  level: 'info',
  streams: [{
    type: 'file',
    path: 'bunyan.log'
  }]
});

// Benchmark Function
async function benchmark(logger, name, iterations = 100000) {
  const start = process.hrtime.bigint();
  
  for (let i = 0; i < iterations; i++) {
    logger.info({
      message: 'Test log entry',
      userId: 12345,
      action: 'user_login',
      timestamp: new Date().toISOString(),
      metadata: { ip: '192.168.1.1', browser: 'Chrome' }
    });
  }
  
  const end = process.hrtime.bigint();
  const duration = Number(end - start) / 1_000_000; // Convert to ms
  const opsPerSecond = (iterations / (duration / 1000)).toFixed(0);
  
  console.log(`\n${name}:`);
  console.log(`  Total time: ${duration.toFixed(2)}ms`);
  console.log(`  Ops/sec: ${opsPerSecond}`);
  console.log(`  Time per log: ${(duration / iterations).toFixed(4)}ms`);
}

// Run benchmarks
(async () => {
  console.log('=== Logging Performance Benchmark ===\n');
  await benchmark(winstonLogger, 'Winston');
  await benchmark(pinoLogger, 'Pino');
  await benchmark(bunyanLogger, 'Bunyan');
})();
```

**Expected Results** (100k iterations):
- **Pino**: ~50-100ms (fastest)
- **Winston**: ~200-400ms
- **Bunyan**: ~150-300ms

---

## Feature Comparison

```javascript
// features-comparison.js

// ============ WINSTON ============
const winston = require('winston');

const winstonLogger = winston.createLogger({
  level: 'debug',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.splat(),
    winston.format.json()
  ),
  defaultMeta: { service: 'user-service' },
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({
      format: winston.format.simple()
    })
  ]
});

// Winston: Multiple transports, custom formatting
winstonLogger.info('User logged in', { userId: 123 });
winstonLogger.error('Database connection failed', { error: new Error('Connection timeout') });

// ============ PINO ============
const pino = require('pino');

const pinoLogger = pino({
  level: 'debug',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      translateTime: 'SYS:standard',
      ignore: 'pid,hostname'
    }
  }
});

// Pino: Child loggers for context
const childLogger = pinoLogger.child({ userId: 123, requestId: 'req-456' });
childLogger.info('User action performed');

// Pino: Extremely fast async logging
pinoLogger.info({ user: 'john', action: 'login' });

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

const bunyanLogger = bunyan.createLogger({
  name: 'myapp',
  level: 'debug',
  streams: [
    {
      level: 'info',
      stream: process.stdout
    },
    {
      level: 'error',
      path: 'error.log'
    }
  ]
});

// Bunyan: Structured logging with fields
bunyanLogger.info({ userId: 123, action: 'login' }, 'User logged in');
bunyanLogger.error({ err: new Error('DB Error') }, 'Database operation failed');

// Bunyan: Child loggers
const childBunyan = bunyanLogger.child({ requestId: 'req-789' });
childBunyan.info('Processing request');
```

---

## Real-World Implementation

```javascript
// logger-factory.js - Unified logging interface

class LoggerFactory {
  static createLogger(type = 'pino', config = {}) {
    switch (type) {
      case 'winston':
        return this.createWinston(config);
      case 'pino':
        return this.createPino(config);
      case 'bunyan':
        return this.createBunyan(config);
      default:
        throw new Error(`Unknown logger type: ${type}`);
    }
  }

  static createWinston(config) {
    const winston = require('winston');
    return winston.createLogger({
      level: config.level || 'info',
      format: winston.format.combine(
        winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
        winston.format.errors({ stack: true }),
        winston.format.json()
      ),
      defaultMeta: { service: config.service || 'app' },
      transports: [
        new winston.transports.File({
          filename: config.errorFile || 'error.log',
          level: 'error'
        }),
        new winston.transports.File({
          filename: config.logFile || 'combined.log'
        }),
        ...(config.console ? [new winston.transports.Console({
          format: winston.format.simple()
        })] : [])
      ]
    });
  }

  static createPino(config) {
    const pino = require('pino');
    return pino({
      level: config.level || 'info',
      transport: {
        target: config.pretty ? 'pino-pretty' : 'pino/file',
        options: config.pretty ? {
          colorize: true,
          translateTime: 'SYS:standard'
        } : {
          destination: config.logFile || 'app.log'
        }
      },
      base: { service: config.service || 'app' }
    });
  }

  static createBunyan(config) {
    const bunyan = require('bunyan');
    const streams = [];

    if (config.console) {
      streams.push({ stream: process.stdout });
    }

    streams.push({
      level: 'error',
      path: config.errorFile || 'error.log'
    });

    streams.push({
      path: config.logFile || 'combined.log'
    });

    return bunyan.createLogger({
      name: config.service || 'app',
      level: config.level || 'info',
      streams
    });
  }
}

module.exports = LoggerFactory;
```

---

## Production Usage Example

```javascript
// app.js - Express application with logging

const express = require('express');
const LoggerFactory = require('./logger-factory');

// Choose logger based on environment
const logger = LoggerFactory.createLogger(
  process.env.LOGGER_TYPE || 'pino',
  {
    level: process.env.LOG_LEVEL || 'info',
    service: 'api-server',
    console: process.env.NODE_ENV !== 'production',
    logFile: 'logs/app.log',
    errorFile: 'logs/error.log'
  }
);

const app = express();

// Middleware: Request logging
app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info({
      method: req.method,
      path: req.path,
      status: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip
    });
  });
  
  next();
});

// Route: User login
app.post('/login', (req, res) => {
  try {
    const { email, password } = req.body;
    
    logger.info({
      event: 'login_attempt',
      email,
      timestamp: new Date().toISOString()
    });

    // Simulate authentication
    if (email && password) {
      logger.info({
        event: 'login_success',
        email,
        userId: 123
      });
      res.json({ success: true, userId: 123 });
    } else {
      throw new Error('Invalid credentials');
    }
  } catch (error) {
    logger.error({
      event: 'login_failed',
      error: error.message,
      stack: error.stack
    });
    res.status(401).json({ error: 'Authentication failed' });
  }
});

// Error handling
app.use((err, req, res, next) => {
  logger.error({
    event: 'unhandled_error',
    error: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method
  });
  res.status(500).json({ error: 'Internal server error' });
});

app.listen(3000, () => {
  logger.info({ event: 'server_started', port: 3000 });
});
```

---

## Decision Matrix

| Criteria | Winston | Pino | Bunyan |
|----------|---------|------|--------|
| **Performance** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Features** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Ease of Use** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Community** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| **JSON Support** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **Async Support** | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |

---

## Recommendations

- **Choose Pino if**: Building high-performance microservices, serverless functions, or applications requiring minimal overhead
- **Choose Winston if**: Need maximum flexibility, multiple transports, or complex logging scenarios
- **Choose Bunyan if**: Prioritizing structured JSON logging with strong debugging capabilities

---

## Conclusion

Pino dominates in performance, Winston excels in flexibility, and Bunyan offers solid structured logging. For most modern Node.js applications, **Pino is the optimal choice** due to its exceptional performance and simplicity, while **Winston remains ideal for complex enterprise applications** requiring extensive customization.
