Back

JavaScriptにおけるエラーログ記録のベストプラクティス

JavaScriptにおけるエラーログ記録のベストプラクティス

本番環境のJavaScriptアプリケーションは、毎日静かに失敗しています。ユーザーは開発者が決して目にすることのないエラーに遭遇し、それが悪いユーザー体験や収益の損失につながっています。これらの問題を捕捉できるアプリケーションとそうでないアプリケーションの違いは何でしょうか?それは適切なエラーログ記録です。

本記事では、フロントエンドとバックエンド環境の両方で堅牢なJavaScriptエラーログ記録を実装するための重要なプラクティスを取り上げます。console.logを超えた方法、実績のあるフレームワークを使用した構造化ログの実装、そしてユーザーが報告する前に重要なエラーを捕捉するシステムの構築方法を学びます。

重要なポイント

  • コンソールログには、本番環境で必要な永続性、集中化、構造が欠けている
  • WinstonやPinoなどのフレームワークを使用した構造化ログにより、分析可能な機械解析可能なデータを提供
  • フロントエンドのエラー処理には、グローバルハンドラーとReact Error Boundariesなどのフレームワーク固有のソリューションが必要
  • 機密データの保護とコンテキスト情報の含有は、効果的なログ記録に不可欠

本番環境でコンソールログが不十分な理由

ほとんどの開発者は、デバッグにconsole.log()を使用することから始めます。開発中は十分ですが、このアプローチは本番環境では失敗します:

// このエラーはユーザーのブラウザの中に消えてしまう
try {
  processPayment(order);
} catch (error) {
  console.error(error); // 本番環境では永遠に失われる
}

コンソールメソッドに欠けているもの:

  • 現在のセッションを超えた永続性
  • ユーザー間での集中的な収集
  • 分析のための構造化データ
  • 優先順位付けのための重要度レベル
  • 機密データの保護

本番アプリケーションには、エラーを捕捉し、構造化し、分析のために中央の場所に送信するログ記録が必要です。

実績のあるフレームワークを使用した構造化ログの実装

適切なフレームワークの選択

Node.jsのログ記録では、2つのフレームワークがエコシステムを支配しています:

**Winston**は、柔軟性と豊富なトランスポートオプションを提供します:

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' })
  ]
});

**Pino**は、最小限のオーバーヘッドでパフォーマンスを優先します:

const pino = require('pino');

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

ログの構造化

機械が解析および分析できるJSON オブジェクトで非構造化文字列を置き換えます:

// 悪い例: 非構造化文字列
logger.info(`User ${userId} failed login attempt`);

// 良い例: 構造化JSON
logger.info({
  event: 'login_failed',
  userId: userId,
  ip: request.ip,
  timestamp: new Date().toISOString(),
  userAgent: request.headers['user-agent']
});

効果的なJavaScriptエラーログ記録の必須コンポーネント

1. 適切なログレベルの使用

アプリケーション全体で一貫した重要度レベルを実装します:

logger.debug('詳細なデバッグ情報');
logger.info('通常のアプリケーションフロー');
logger.warn('警告: パフォーマンスの低下を検出');
logger.error('エラーが発生したがアプリケーションは継続');
logger.fatal('致命的な障害、アプリケーションをシャットダウン');

2. 常にスタックトレースを含める

デバッグのために完全なエラーコンテキストを捕捉します:

process.on('uncaughtException', (error) => {
  logger.fatal({
    message: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString()
  });
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error({
    message: 'Unhandled Promise Rejection',
    reason: reason,
    promise: promise
  });
});

3. コンテキスト情報の追加

問題を追跡するために、リクエストID、ユーザーID、セッションデータを含めます:

const requestLogger = logger.child({
  requestId: generateRequestId(),
  sessionId: request.session.id
});

requestLogger.info('Processing payment request');

4. 機密データの保護

パスワード、トークン、個人情報を決してログに記録しないでください:

const logger = pino({
  redact: ['password', 'creditCard', 'ssn', 'authorization']
});

// これらのフィールドは自動的に編集される
logger.info({
  user: email,
  password: 'secret123', // [REDACTED]として表示される
  action: 'login_attempt'
});

フロントエンドエラー処理戦略

グローバルエラーハンドラー

ブラウザ環境でのすべての未処理エラーを捕捉します:

window.addEventListener('error', (event) => {
  logToServer({
    message: event.message,
    source: event.filename,
    line: event.lineno,
    column: event.colno,
    stack: event.error?.stack
  });
});

window.addEventListener('unhandledrejection', (event) => {
  logToServer({
    type: 'unhandledRejection',
    reason: event.reason,
    promise: event.promise
  });
});

React Error Boundaries

Reactアプリケーションの場合、コンポーネントエラーを捕捉するためにエラーバウンダリを実装します:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logger.error({
      message: error.toString(),
      componentStack: errorInfo.componentStack,
      timestamp: new Date().toISOString()
    });
  }

  render() {
    if (this.state.hasError) {
      return <h2>問題が発生しました。ページを更新してください。</h2>;
    }
    return this.props.children;
  }
}

分析のためのログの集中化

すべてのログをstdoutに送信し、インフラストラクチャにルーティングを処理させます:

// stdoutのみに出力するようにロガーを設定
const logger = pino({
  transport: {
    target: 'pino-pretty',
    options: {
      destination: 1 // stdout
    }
  }
});

このアプローチにより、Docker、Kubernetes、またはFluentdなどのログシッパーが、分析のためにログを集中システムに収集およびルーティングできます。クライアント側アプリケーションの場合、ブラウザから集中ログインフラストラクチャにログを受信および転送するシンプルなエンドポイントを実装します。

まとめ

効果的なJavaScriptエラーログ記録には、console.logをフレームワークに置き換える以上のことが必要です。構造化データ、適切な重要度レベル、包括的なエラーコンテキスト、集中収集が求められます。WinstonやPinoなどのフレームワークでこれらのプラクティスを実装し、機密データを保護し、フロントエンドコードに適切なエラーバウンダリを確立することで、ユーザーに影響を与える前に問題を捕捉するシステムを構築できます。これらの基本から始めて、アプリケーション固有の監視ニーズに基づいて拡張してください。

よくある質問

Pinoは最小限のオーバーヘッドで、ほとんどの場合2〜3%のレイテンシしか追加しません。Winstonはわずかに重いですが、ほとんどのアプリケーションでは依然として無視できる程度です。どちらも本番環境に対応しており、世界中の高トラフィックアプリケーションで使用されています。

ログフレームワークに組み込まれている編集機能を使用して、機密フィールドを自動的にマスクします。パスワード、トークン、クレジットカード番号などの編集するフィールド名のリストを定義します。偶発的なデータ露出がないか、常にログを定期的に監査してください。

両側でエラーをログに記録してください。クライアント側のログ記録は、ブラウザ固有の問題とサーバーに到達しないJavaScriptエラーを捕捉します。サーバー側のログ記録は、APIエラーとバックエンドの障害を処理します。両方のソースを集約するために集中システムを使用してください。

errorおよびfatalレベルのアラートを即座に設定してください。warningレベルは日次サマリーをトリガーできます。infoおよびdebugレベルは検索可能であるべきですが、特定の問題を調査している場合や重要なビジネスイベントを監視している場合を除き、アラートをトリガーすべきではありません。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay