Guide/guides

ロギングガイド

Guren はRFC 5424に準拠したログレベル、複数のチャンネル、コンテキスト付きロギングをサポートする柔軟なロギングシステムを提供します。

ロギングガイド

Guren はRFC 5424に準拠したログレベル、複数のチャンネル、コンテキスト付きロギングをサポートする柔軟なロギングシステムを提供します。

コアコンセプト

  • LogManager – ロギングチャンネルを管理する中央ハブ。
  • Logger – コンテキストサポート付きでログエントリを書き込むインスタンス。
  • LogChannel – ログエントリの出力先(コンソール、ファイルなど)。
  • LogLevel – RFC 5424に準拠した重大度レベル(emergency、alert、critical、error、warning、notice、info、debug)。

基本的な使い方

ファサードを使用(推奨)

LogManager を使うと、コンテナから LogManager を遅延解決してシンプルにログ出力できます。

// Resolve the logger from the container
const Log = app.container.make('log')

Log.info('アプリケーションが起動しました')
Log.error('問題が発生しました', { error: '接続に失敗しました' })
Log.channel('file').warning('ファイルにのみ記録')

クイックスタート

LogManager を直接インスタンス化して使うこともできます。

import { LogManager } from '@guren/core'

const log = new LogManager({
  default: 'console',
  channels: {
    console: { driver: 'console', level: 'debug' },
  },
})

log.info('アプリケーションが起動しました')
log.error('問題が発生しました', { error: '接続に失敗しました' })

ログレベル

Guren はRFC 5424の重大度レベルをサポートしています(最も重大なものから)。

log.emergency('システムが使用不能')
log.alert('即時対応が必要')
log.critical('重大な状態')
log.error('エラー状態')
log.warning('警告状態')  // または log.warn()
log.notice('正常だが重要な状態')
log.info('情報メッセージ')
log.debug('デバッグレベルのメッセージ')

コンテキスト付きロギング

// 個別のログエントリにコンテキストを追加
log.info('ユーザーがログイン', { userId: 123, ip: '192.168.1.1' })

// 永続的なコンテキストを持つロガーを作成
const requestLog = log.withContext({ requestId: 'abc-123' })
requestLog.info('リクエスト処理中')    // requestIdを含む
requestLog.info('リクエスト完了')     // requestIdを含む

// 子ロガーを作成
const userLog = requestLog.child({ userId: 456 })
userLog.info('ユーザーアクション')  // requestIdとuserIdを含む

チャンネル

コンソールチャンネル

色とフォーマット付きでコンソールに出力します。

const log = new LogManager({
  default: 'console',
  channels: {
    console: {
      driver: 'console',
      level: 'debug',          // ログ出力の最小レベル
      colors: true,            // ANSIカラーを有効化
      timestamps: true,        // タイムスタンプを含める
      format: 'text',          // 'text' または 'json'
    },
  },
})

ファイルチャンネル

単一ファイルにログを書き込みます。

const log = new LogManager({
  default: 'file',
  channels: {
    file: {
      driver: 'file',
      path: './storage/logs/app.log',
      level: 'info',
      format: 'json',  // 'text' または 'json'
    },
  },
})

デイリーファイルチャンネル

自動クリーンアップ付きで毎日ログファイルをローテーションします。

const log = new LogManager({
  default: 'daily',
  channels: {
    daily: {
      driver: 'daily',
      path: './storage/logs/app.log',
      days: 14,        // 14日間ログを保持
      level: 'info',
      format: 'json',
    },
  },
})

ファイル名には日付サフィックスが付きます:app-2024-01-15.log

スタックチャンネル

複数のチャンネルを組み合わせることができます。

const log = new LogManager({
  default: 'stack',
  channels: {
    console: { driver: 'console', level: 'debug' },
    file: { driver: 'daily', path: './storage/logs/app.log', days: 14 },
    stack: {
      driver: 'stack',
      channels: ['console', 'file'],  // 両方に書き込み
    },
  },
})

複数チャンネルの使用

チャンネルの切り替え

// デフォルトチャンネルを使用
log.info('デフォルトを使用')

// 特定のチャンネルを使用
log.channel('console').debug('デバッグ情報')
log.channel('file').error('ファイルのみに記録')

一時的なスタック

// 一時的なスタックを作成
log.stack(['console', 'file']).critical('重大なエラー!')

設定

完全な設定例

import { LogManager } from '@guren/core'

const log = new LogManager({
  default: 'stack',
  channels: {
    // 開発用コンソール
    console: {
      driver: 'console',
      level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
      colors: true,
      timestamps: true,
      format: 'text',
    },

    // 本番用の日次ローテーションファイル
    daily: {
      driver: 'daily',
      path: './storage/logs/app.log',
      level: 'info',
      days: 30,
      format: 'json',
    },

    // エラー専用ログ
    errors: {
      driver: 'daily',
      path: './storage/logs/errors.log',
      level: 'error',
      days: 90,
      format: 'json',
    },

    // デフォルトロギング用スタック
    stack: {
      driver: 'stack',
      channels: ['console', 'daily'],
    },

    // 重大アラート
    critical: {
      driver: 'stack',
      channels: ['console', 'daily', 'errors'],
    },
  },
})

グローバルロガー

グローバルロガーのセットアップ

import { setLogManager, getLogManager, LogManager } from '@guren/core'

// ブートストラップファイルで
const log = new LogManager({
  default: 'stack',
  channels: { /* ... */ },
})

setLogManager(log)

// アプリケーションのどこでも
const log = getLogManager()
log.info('グローバルロガーを使用')

カスタムチャンネル

カスタムチャンネルの作成

import type { LogChannel, LogEntry } from '@guren/core'

class SlackChannel implements LogChannel {
  constructor(private webhookUrl: string) {}

  async log(entry: LogEntry): Promise<void> {
    if (entry.level !== 'critical' && entry.level !== 'emergency') {
      return // critical/emergencyのみSlackに送信
    }

    await fetch(this.webhookUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `[${entry.level.toUpperCase()}] ${entry.message}`,
        attachments: [{
          fields: Object.entries(entry.context).map(([k, v]) => ({
            title: k,
            value: String(v),
            short: true,
          })),
        }],
      }),
    })
  }
}

カスタムドライバーの登録

const log = new LogManager({
  default: 'stack',
  channels: {
    slack: { driver: 'slack', webhookUrl: process.env.SLACK_WEBHOOK },
    stack: { driver: 'stack', channels: ['console', 'slack'] },
  },
})

log.registerDriver('slack', (config) => {
  return new SlackChannel(config.webhookUrl as string)
})

リクエストロギング

ミドルウェアの例

import { defineMiddleware } from '@guren/core'
import { getLogManager } from '@guren/core'

export const requestLogging = defineMiddleware(async (c, next) => {
  const log = getLogManager()
  const requestId = crypto.randomUUID()
  const requestLog = log.withContext({
    requestId,
    method: c.req.method,
    path: c.req.path,
  })

  const start = Date.now()
  requestLog.info('リクエスト開始')

  // ハンドラーで使用するためコンテキストにロガーを保存
  c.set('log', requestLog)

  try {
    await next()

    requestLog.info('リクエスト完了', {
      status: c.res.status,
      duration: Date.now() - start,
    })
  } catch (error) {
    requestLog.error('リクエスト失敗', {
      error: error instanceof Error ? error.message : '不明なエラー',
      duration: Date.now() - start,
    })
    throw error
  }
})

テスト

import { describe, test, expect, beforeEach, mock } from 'bun:test'
import { LogManager, Logger, LOG_LEVEL_PRIORITY } from '@guren/core'
import type { LogChannel, LogEntry } from '@guren/core'

// テスト用メモリチャンネルを作成
class MemoryChannel implements LogChannel {
  entries: LogEntry[] = []

  log(entry: LogEntry): void {
    this.entries.push(entry)
  }

  clear(): void {
    this.entries = []
  }
}

describe('ロギング', () => {
  let memoryChannel: MemoryChannel
  let log: LogManager

  beforeEach(() => {
    memoryChannel = new MemoryChannel()
    log = new LogManager({
      default: 'memory',
      channels: {
        memory: { driver: 'memory' },
      },
    })
    log.registerDriver('memory', () => memoryChannel)
  })

  test('異なるレベルでログを記録する', () => {
    log.info('情報メッセージ')
    log.error('エラーメッセージ')

    expect(memoryChannel.entries).toHaveLength(2)
    expect(memoryChannel.entries[0].level).toBe('info')
    expect(memoryChannel.entries[1].level).toBe('error')
  })

  test('ログエントリにコンテキストを含める', () => {
    log.info('ユーザーアクション', { userId: 123 })

    expect(memoryChannel.entries[0].context).toEqual({ userId: 123 })
  })

  test('withContextが永続的なコンテキストを追加する', () => {
    const requestLog = log.withContext({ requestId: 'abc' })
    requestLog.info('最初')
    requestLog.info('2番目', { extra: 'data' })

    expect(memoryChannel.entries[0].context).toEqual({ requestId: 'abc' })
    expect(memoryChannel.entries[1].context).toEqual({ requestId: 'abc', extra: 'data' })
  })
})

ログ出力フォーマット

テキストフォーマット

[2024-01-15T10:30:00.000Z] INFO      ユーザーがログイン {"userId":123,"ip":"192.168.1.1"}
[2024-01-15T10:30:01.000Z] ERROR     データベース接続失敗 {"error":"ECONNREFUSED"}

JSONフォーマット

{"timestamp":"2024-01-15T10:30:00.000Z","level":"info","message":"ユーザーがログイン","userId":123,"ip":"192.168.1.1"}
{"timestamp":"2024-01-15T10:30:01.000Z","level":"error","message":"データベース接続失敗","error":"ECONNREFUSED"}

ベストプラクティス

  1. 適切なログレベルを使用: すべてをerrorとして記録しない。開発情報にはdebugを使用。

  2. コンテキストを含める: デバッグのため常に関連するコンテキスト(ユーザーID、リクエストIDなど)を追加。

  3. 構造化ロギングを使用: 本番環境ではJSONフォーマットの方が解析・検索が容易。

  4. ログファイルをローテーション: ディスク容量の問題を防ぐためdailyドライバーを使用。

  5. 本番環境ではレベルでフィルタリング: 本番環境ではコンソールを'info'以上に設定。

  6. コンテキスト付きロガーを作成: リクエストスコープのロギングにはwithContext()を使用。

  7. 機密データをログに記録しない: パスワード、トークン、個人情報は決してログに記録しない。

  8. シャットダウン時にチャンネルを閉じる: 保留中の書き込みをフラッシュするためlog.close()を呼び出す。