ロギングベストプラクティス - 効果的なログ設計と運用

16分 で読める | 2025.01.10

なぜロギングが重要か

適切なログは、本番環境での問題特定を数時間から数分に短縮します。逆に不適切なログは、ノイズとなり問題を見つけにくくします。

ログレベルの使い分け

レベル用途
ERROR即座の対応が必要データベース接続失敗、API障害
WARN潜在的な問題リトライ発生、非推奨機能使用
INFO重要なビジネスイベントユーザー登録、決済完了
DEBUG開発時のデバッグ情報変数の値、処理フロー
TRACE詳細なトレース情報メソッド呼び出し詳細
// 適切なログレベル使用例
logger.error('Payment failed', { orderId, error: err.message });
logger.warn('Retry attempt', { attempt: 3, maxRetries: 5 });
logger.info('User registered', { userId, plan: 'premium' });
logger.debug('Processing order items', { items });

構造化ログ

JSONログフォーマット

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

// 出力例
// {"level":"info","time":"2025-01-10T12:00:00.000Z","msg":"User login","userId":"123","ip":"192.168.1.1"}

logger.info({ userId: '123', ip: '192.168.1.1' }, 'User login');

コンテキスト情報の付加

// リクエストコンテキストを自動付加
function createRequestLogger(req: Request) {
  return logger.child({
    requestId: req.headers['x-request-id'],
    userId: req.user?.id,
    path: req.path,
    method: req.method,
  });
}

// 使用
app.use((req, res, next) => {
  req.log = createRequestLogger(req);
  next();
});

app.get('/orders', (req, res) => {
  req.log.info('Fetching orders');  // 自動的にrequestId等が付加
});

機密情報の除外

const sensitiveFields = ['password', 'token', 'creditCard', 'ssn'];

function sanitizeLogData(data: Record<string, any>): Record<string, any> {
  const sanitized = { ...data };

  for (const field of sensitiveFields) {
    if (field in sanitized) {
      sanitized[field] = '[REDACTED]';
    }
  }

  // ネストされたオブジェクトも処理
  for (const [key, value] of Object.entries(sanitized)) {
    if (typeof value === 'object' && value !== null) {
      sanitized[key] = sanitizeLogData(value);
    }
  }

  return sanitized;
}

// 使用
logger.info(sanitizeLogData({ userId: '123', password: 'secret' }), 'Login attempt');
// 出力: {"userId":"123","password":"[REDACTED]","msg":"Login attempt"}

分散トレーシング連携

import { trace, context } from '@opentelemetry/api';

function getTraceContext() {
  const span = trace.getSpan(context.active());
  if (!span) return {};

  const spanContext = span.spanContext();
  return {
    traceId: spanContext.traceId,
    spanId: spanContext.spanId,
  };
}

// ログに自動的にトレース情報を付加
const tracingLogger = logger.child(getTraceContext());

エラーログのベストプラクティス

// 悪い例
try {
  await processPayment(order);
} catch (error) {
  console.log('Error');  // 情報不足
}

// 良い例
try {
  await processPayment(order);
} catch (error) {
  logger.error({
    err: error,
    orderId: order.id,
    amount: order.amount,
    paymentMethod: order.paymentMethod,
    stack: error.stack,
  }, 'Payment processing failed');

  // エラー種別に応じた処理
  if (error instanceof PaymentDeclinedError) {
    logger.warn({ orderId: order.id }, 'Payment declined by provider');
  }
}

ログ集約アーキテクチャ

flowchart LR
    subgraph Apps["アプリケーション"]
        A1["Service A"]
        A2["Service B"]
        A3["Service C"]
    end

    subgraph Collect["収集層"]
        FB["Fluentd/Fluent Bit"]
    end

    subgraph Store["保存層"]
        ES["Elasticsearch"]
    end

    subgraph View["可視化層"]
        KB["Kibana"]
    end

    Apps --> FB --> ES --> KB

パフォーマンス考慮

// 高頻度ログはサンプリング
let requestCount = 0;
const SAMPLE_RATE = 100;

app.use((req, res, next) => {
  requestCount++;
  if (requestCount % SAMPLE_RATE === 0) {
    logger.debug({ path: req.path }, 'Request sample');
  }
  next();
});

// 非同期ログ出力
const logger = pino({
  transport: {
    target: 'pino/file',
    options: { destination: '/var/log/app.log' },
  },
});

現場での教訓

  1. 本番ではINFO以上 - DEBUGログは本番で有効にしない
  2. requestIdは必須 - 問題追跡に不可欠
  3. ログローテーション設定 - ディスク枯渇を防ぐ

関連記事

まとめ

効果的なロギングは、構造化ログ、適切なログレベル、コンテキスト情報の付加が鍵です。機密情報の除外とパフォーマンスにも注意を払いましょう。

← 一覧に戻る