Bunyan

構造化JSON中心のNode.jsロギングライブラリ。「ログは文字列ではなくJSONオブジェクトであるべき」という哲学に基づき設計。優秀なCLIツールによる美しい表示とフィルタリング機能、本番環境の問題分析に特化した機能を提供。

ロギングライブラリJavaScriptNode.jsJSON特化構造化ログストリーム

GitHub概要

trentm/node-bunyan

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

スター7,200
ウォッチ114
フォーク517
作成日:2012年1月30日
言語:JavaScript
ライセンス:Other

トピックス

なし

スター履歴

trentm/node-bunyan Star History
データ取得日時: 2025/7/17 10:01

ライブラリ

Bunyan

概要

BunyanはNode.js向けの構造化JSON特化ログライブラリです。JSON形式での出力に特化し、構造化データ、階層的ロガー、ログストリーミング、コマンドラインビューアーを提供します。JSON-firstアプローチによりログ解析と可観測性に優れ、全てのログエントリが一貫したJSON構造を持つため、機械的な分析と処理が容易です。ストリームベースアーキテクチャにより柔軟な出力先対応、シリアライザーによる共通オブジェクトの自動フォーマット、子ロガーによるコンテキスト継承を実現します。

詳細

Bunyan 2025年において構造化ログを重視するNode.js開発で重要な選択肢として位置づけられています。JSON-firstのアプローチにより、ELK Stack、Splunk等のログ分析プラットフォームとの親和性が高く、マイクロサービスでの分散ログ管理に適しています。各ログレコードは1行のJSON.stringify'd出力となり、自動フィールド(pid、hostname、time、v)が付与され、ErrorオブジェクトやHTTPリクエスト等の共通オブジェクトは専用シリアライザーで処理されます。Google Cloud Logging等のクラウドサービスとの統合も充実し、現代的な可観測性要件に対応しています。

主な特徴

  • JSON-First設計: 全ログエントリが一貫したJSON構造による機械解析の容易さ
  • 組み込みCLIツール: 開発時のログ表示・フィルタリング用コマンドラインツール
  • シリアライザー: Error、HTTPリクエスト等の共通オブジェクトの自動JSON変換
  • ストリームベース: Node.jsストリームによる柔軟な出力先制御
  • 子ロガー: コンテキスト固有ロガーによるフィールド継承とスコープ管理
  • 6段階ログレベル: trace、debug、info、warn、error、fatalの階層制御

メリット・デメリット

メリット

  • JSON形式による機械可読ログで自動分析・処理が極めて容易
  • ELK Stack、Splunk等のログ分析プラットフォームとの高い親和性
  • 構造化データによりマイクロサービスでの分散ログ管理に最適
  • シリアライザーによる複雑オブジェクトの自動適切フォーマット
  • 子ロガー機能によりコンテキスト情報の効率的な管理
  • Google Cloud Logging等のクラウドサービスとの標準統合
  • CLI ツールによる開発時のログ表示・デバッグ支援

デメリット

  • 現在活発な保守が行われておらず、新機能追加が期待できない
  • JSON出力のため人間が直接読む際の可読性が劣る
  • ファイルサイズがプレーンテキストログより大きくなる傾向
  • 軽量なロギングが必要な用途にはオーバースペック
  • Winston、Pinoと比較して機能の豊富さで劣る部分がある
  • レガシープロジェクトでは既存ログフォーマットとの互換性課題

参考ページ

書き方の例

インストールと基本セットアップ

# Bunyanのインストール
npm install bunyan

# 開発用のCLIツールをグローバルインストール(オプション)
npm install -g bunyan
// ES6モジュール(推奨)
import bunyan from 'bunyan';

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

// 最もシンプルなロガー作成
const log = bunyan.createLogger({name: 'myapp'});

// 基本的なログ出力
log.trace('トレースメッセージ');
log.debug('デバッグメッセージ');
log.info('情報メッセージ');
log.warn('警告メッセージ');
log.error('エラーメッセージ');
log.fatal('致命的エラーメッセージ');

// 出力例(JSON形式)
// {"name":"myapp","hostname":"localhost","pid":1234,"level":30,"msg":"情報メッセージ","time":"2025-01-01T12:00:00.000Z","v":0}

// 構造化データのログ
log.info({user_id: 123, action: 'login'}, 'ユーザーログイン');

基本的なロギング操作(レベル、フォーマット)

import bunyan from 'bunyan';

// 詳細設定付きロガー
const log = bunyan.createLogger({
  name: 'web-service',
  level: 'info',
  src: true,  // ソースファイル情報を含める(開発時のみ推奨)
});

// レベル別ログ出力
log.trace({component: 'database'}, 'トレースレベルのデバッグ情報');
log.debug({sql: 'SELECT * FROM users'}, 'SQL実行デバッグ');
log.info({user_id: 456}, 'ユーザー操作ログ');
log.warn({threshold: 1000, actual: 1500}, '応答時間警告');
log.error({error_code: 'DB_001'}, 'データベースエラー');
log.fatal({system: 'payment'}, 'システム致命的エラー');

// オブジェクトと文字列メッセージの組み合わせ
log.info({
  request_id: 'req_123',
  method: 'POST',
  url: '/api/users',
  duration_ms: 150
}, 'リクエスト処理完了');

// 条件付きログ(パフォーマンス考慮)
if (log.trace()) {
  const heavyDebugData = generateExpensiveDebugData();
  log.trace({debug_data: heavyDebugData}, '重い処理のデバッグ情報');
}

// レベル設定の動的変更
log.level(bunyan.DEBUG);  // DEBUGレベル以上を出力
log.level('warn');        // WARNレベル以上を出力

// ログレベル確認
console.log('現在のログレベル:', log.level());
console.log('DEBUGは有効?', log.debug());
console.log('INFOは有効?', log.info());

高度な設定とカスタマイズ(ストリーム、シリアライザー等)

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

// 複数ストリーム設定
const log = bunyan.createLogger({
  name: 'multi-stream-app',
  streams: [
    {
      level: 'debug',
      stream: process.stdout  // コンソール出力
    },
    {
      level: 'info',
      path: './logs/app.log'  // ファイル出力
    },
    {
      level: 'error',
      path: './logs/error.log'  // エラー専用ファイル
    }
  ]
});

// ローテーティングファイルストリーム
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',        // 日次ローテーション
      totalFiles: 10,      // 最大10ファイル保持
      rotateExisting: true,
      gzip: true          // 古いファイルを圧縮
    })
  }]
});

// カスタムシリアライザー
const log = bunyan.createLogger({
  name: 'custom-serializers',
  serializers: {
    // HTTPリクエストシリアライザー
    req: (req) => ({
      method: req.method,
      url: req.url,
      headers: req.headers,
      remoteAddress: req.connection.remoteAddress,
      remotePort: req.connection.remotePort
    }),
    
    // HTTPレスポンスシリアライザー
    res: (res) => ({
      statusCode: res.statusCode,
      headers: res.getHeaders()
    }),
    
    // カスタムユーザーオブジェクトシリアライザー
    user: (user) => ({
      id: user.id,
      username: user.username,
      role: user.role,
      // パスワードなど機密情報は除外
    }),
    
    // エラーシリアライザー(デフォルトを拡張)
    err: bunyan.stdSerializers.err
  }
});

// シリアライザー使用例
log.info({
  req: request,
  res: response,
  user: currentUser
}, 'API呼び出し完了');

// カスタムストリーム(例: Slack通知)
class SlackStream {
  constructor(webhookUrl) {
    this.webhookUrl = webhookUrl;
  }
  
  write(record) {
    // エラーレベル以上の場合のみSlackに通知
    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通知エラー:', error);
    }
  }
}

// Slackストリーム使用例
const slackLog = bunyan.createLogger({
  name: 'slack-notifier',
  streams: [
    { stream: process.stdout },
    { 
      type: 'raw',
      stream: new SlackStream('https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK')
    }
  ]
});

構造化ログと現代的な可観測性対応

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

// 分散トレーシング対応ロガー
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
      }
    });
  }
}

// 使用例
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'
}, '注文処理開始');

// OpenTelemetryスタイルのログ
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;
  }
}

// メトリクス統合ロガー
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}`);
}

// ビジネスメトリクス記録
recordMetric('orders.completed', 1, 'counter', {
  payment_method: 'credit_card',
  customer_tier: 'premium'
});

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

// 構造化イベントログ
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
    })
  }
});

// セキュリティイベント
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');

// ユーザーアクションイベント
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');

エラーハンドリングとパフォーマンス最適化

import bunyan from 'bunyan';

// パフォーマンス最適化ロガー
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
    });
    
    // 定期的なフラッシュ
    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();
  }
  
  // 便利メソッド
  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);
  }
}

// エラーハンドリング強化ロガー
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; // 成功時はエラーカウントリセット
    } 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); }
}

// 非同期ログ処理
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);
          // 次のティックで解決(非ブロッキング)
          process.nextTick(resolve);
        });
        
        logEntry.resolve();
      } catch (error) {
        logEntry.reject(error);
      }
    }
    
    this.processing = false;
  }
  
  // Promise版便利メソッド
  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); }
}

// パフォーマンス測定ロガー
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;
      }
    }
  };
}

// 使用例
const perfLogger = createPerformanceLogger('performance-test');
const robustLogger = new RobustLogger('robust-test');
const asyncLogger = new AsyncLogger('async-test');

// パフォーマンス測定
const result = await perfLogger.measure('database_query', async () => {
  // データベース処理のシミュレーション
  await new Promise(resolve => setTimeout(resolve, 100));
  return { data: 'query_result' };
}, { user_id: 123 });

// 非同期ログ
await asyncLogger.info({ user_id: 456 }, '非同期ログテスト');

// ロバストログ
robustLogger.error({ error_code: 'SYS_001' }, 'システムエラー発生');

フレームワーク統合と実用例

// Express.js統合例
import express from 'express';
import bunyan from 'bunyan';

const app = express();

// リクエストロギング用ロガー
const requestLogger = bunyan.createLogger({
  name: 'express-app',
  serializers: {
    req: bunyan.stdSerializers.req,
    res: bunyan.stdSerializers.res
  }
});

// リクエストロギングミドルウェア
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();
});

// エラーハンドリングミドルウェア
app.use((error, req, res, next) => {
  req.log.error({
    err: error,
    req,
    res
  }, 'Request error');
  
  res.status(500).json({ error: 'Internal Server Error' });
});

// ルートハンドラー例
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統合例
import Hapi from '@hapi/hapi';

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

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

// プラグイン形式でのロガー統合
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統合例
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統合例
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) {
  // ジョブ処理ロジック
  return { status: 'completed', processed_at: new Date() };
}