Bunyan
構造化JSON中心のNode.jsロギングライブラリ。「ログは文字列ではなくJSONオブジェクトであるべき」という哲学に基づき設計。優秀なCLIツールによる美しい表示とフィルタリング機能、本番環境の問題分析に特化した機能を提供。
GitHub概要
trentm/node-bunyan
a simple and fast JSON logging module for node.js services
スター7,200
ウォッチ114
フォーク517
作成日:2012年1月30日
言語:JavaScript
ライセンス:Other
トピックス
なし
スター履歴
データ取得日時: 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() };
}