TypeScriptで構築するWebアプリのログが追えない問題を解消!適切なログレベルとJSON構造化を実現するベストプラクティス

TypeScriptのコードと整理されたJSON形式のログが並ぶブログのアイキャッチ画像 バックエンド

本番環境で障害が発生したとき、まず頼りになるのはログです。
しかし、いざコンソールやログ管理ツールを開いてみると、console.logが大量に並んでいるだけで、どれが重要な情報なのか分からない——そんな経験はありませんか。

TypeScriptでWebアプリを構築していると、開発中は動作確認用に気軽にログを仕込みがちです。
ですが、そのログレベルを意識せずに本番環境まで持ち込んでしまうと、重大なエラーと単なるデバッグ情報が同列に扱われ、本当に追うべき問題を見失う原因になります。
さらに、ログの形式が統一されていなければ、検索性や集計性も著しく低下します。

なぜログが「読めない」状態に陥るのか、その根本原因は主に2つあります。

  • ログレベルの設計が曖昧で、運用者が優先度を判断できない
  • テキストの羅列でログを出力しており、機械的なパースや検索に不向きである

これらは個別の問題ではなく、相互に関連しています。
ログレベルを適切に設計しても、出力形式が非構造化のままでは、監視ツールでのフィルタリングや集計に手間がかかります。
逆に構造化だけを行っても、レベル設計が甘ければ、ノイズの多いログの中から重要な情報を掘り起こす作業は変わりません。

本記事では、TypeScriptで構築するWebアプリを対象に、ログレベルの適切な設計指針と、JSON構造化によるログ出力のベストプラクティスを、実装例を交えながら解説していきます。
運用に耐えうるログ基盤を整え、障害調査の効率を根本から改善したいエンジニアの方は、ぜひ参考にしてください。

  1. なぜTypeScriptのWebアプリで「ログが追えない」問題が起きるのか
    1. console.logの多用がもたらす弊害
    2. ログレベルが未設計であることのリスク
    3. 非構造化ログが引き起こす検索性の低下
  2. ログレベルとは何か?基本の5段階を押さえる
    1. DEBUG・INFO・WARN・ERROR・FATALの役割の違い
    2. 環境ごとに出力レベルを切り替える考え方
  3. TypeScriptプロジェクトにおけるログレベル設計の実践指針
    1. 業務ロジックとインフラ層で分けるログレベルの粒度
    2. 過剰なログとログ不足のバランスの取り方
  4. JSON構造化ログとは?テキストログとの違いを比較
    1. 構造化ログのメリット
    2. 構造化ログのデメリットと注意点
  5. TypeScriptでJSON構造化ログを実装する方法
    1. pinoを使った構造化ログの実装例
    2. winstonを使った構造化ログの実装例
    3. 独自ロガーを自作する場合の設計ポイント
  6. 構造化ログに含めるべき必須フィールドの設計
    1. timestamp・level・messageの基本構成
    2. traceIdやrequestIdによるリクエスト追跡
    3. エラーオブジェクトのシリアライズ方法
  7. 本番運用で活きるログ管理・監視ツールとの連携
    1. CloudWatch LogsやDatadogとの連携方法
    2. ログのローテーションと保存コストの最適化
  8. ログ設計・運用時によくある失敗と回避策
    1. 個人情報や機密情報のログ出力による情報漏洩
    2. パフォーマンス低下を招く同期的ログ出力
  9. まとめ:適切なログレベルとJSON構造化で障害調査を効率化しよう

なぜTypeScriptのWebアプリで「ログが追えない」問題が起きるのか

TypeScriptのコードとエラーログが乱雑に表示されている様子のイラスト

TypeScriptは静的型付けによってコンパイル時の安全性を高めてくれますが、実行時に何が起きているかを可視化するのはログの役目です。
型システムがどれだけ堅牢でも、ログ設計が疎かであれば、障害発生時に原因を追跡する術がありません。
ここでは、なぜ多くのWebアプリで「ログが追えない」状態に陥ってしまうのか、その根本要因を3つの観点から整理していきます。

console.logの多用がもたらす弊害

開発中、動作確認のためにconsole.logを挿入するのは自然な行為です。
しかし、それを削除せずに本番環境まで持ち込んでしまうケースは少なくありません。

console.logには、ログレベルという概念が存在しません。
デバッグ用の一時的な出力も、重大なエラー通知も、すべて同じ扱いになってしまいます。
その結果、以下のような問題が発生します。

  • 本番環境で不要な情報がログに大量に混入する
  • 重要なエラーが埋もれてしまい、気づくのが遅れる
  • 出力先やフォーマットを制御できず、監視ツールとの連携が困難になる

コードの品質を保証するには、ログ出力そのものにも設計思想を持ち込む必要があります。
console.logはあくまで一時的なデバッグ手段であり、恒久的な運用ログとして採用すべきものではない、と理解しておくべきでしょう。

ログレベルが未設計であることのリスク

ログレベルとは、出力される情報の重要度を段階的に分類する仕組みです。
この設計が甘いと、運用者は膨大なログの中から本当に対応すべき情報を判断できなくなります。

例えば、致命的なエラーと単なる処理の進捗通知が同列に扱われていたらどうでしょうか。
障害調査の初動が遅れ、復旧までの時間が無駄に長引いてしまいます。
ログレベルの設計は、単なる技術的な作法ではなく、システムの信頼性を左右する重要な意思決定なのです。

適切な設計がなされていれば、フィルタリングによって必要な情報だけを瞬時に抽出できます。
逆に設計が存在しなければ、フィルタリング自体が機能しません。
この差は、障害対応のスピードに直結します。

非構造化ログが引き起こす検索性の低下

ログレベルを整えたとしても、出力形式が自由記述のテキストのままでは限界があります。
人間が目視で読む分には問題なくても、機械的な検索や集計には不向きだからです。

例えば、特定のユーザーIDに紐づくエラーだけを抽出したい場合、非構造化ログでは正規表現による曖昧な文字列検索に頼らざるを得ません。
フィールドの表記揺れや順序の違いがあれば、検索漏れも発生します。

この問題を解決するのが、次のセクションで扱うJSON構造化ログという考え方です。
ログを機械可読なデータとして扱うことで、検索性と集計性を根本から改善できます。

ログレベルとは何か?基本の5段階を押さえる

DEBUGからFATALまでのログレベルが階段状に並んでいる図

ログレベルとは、出力される情報の重要度や性質を段階的に分類するための指標です。
多くのロギングライブラリでは、この分類にもとづいてログの出力可否をフィルタリングできます。
まずは基本となる5段階の意味を正確に理解しておきましょう。

DEBUG・INFO・WARN・ERROR・FATALの役割の違い

ログレベルは一般的に、以下の5段階で構成されます。
それぞれの役割を混同してしまうと、せっかくレベルを分けても意味がありません。

レベル 用途 本番環境での扱い
DEBUG 開発時の詳細な処理追跡 通常は非表示
INFO 正常な処理の進行状況 記録するが軽視可能
WARN 想定外だが処理継続可能な事象 注意深く監視
ERROR 処理が失敗した重大な事象 即座に対応
FATAL システム全体が継続不能な致命的事象 最優先で対応

DEBUGは、変数の中身や処理の分岐といった、開発者がコードの挙動を細かく追うための情報です。
INFOは、リクエストの受付やバッチ処理の開始・終了など、システムが正常に稼働していることを示す記録に用います。

WARNは注意が必要ですが、システムを止めるほどではない事象、例えば非推奨のAPIが呼び出された場合などに使います。
ERRORは、処理が失敗し、ユーザーへの影響が出ている状態を示すもの。
そしてFATALは、プロセス自体が継続できないほどの致命的な障害を意味します。

この粒度を正しく使い分けることで、ログを読む側は文脈を読まなくても、事象の深刻度を瞬時に判断できるようになります。

環境ごとに出力レベルを切り替える考え方

開発環境と本番環境では、必要とされる情報の粒度が異なります。
開発中はDEBUGレベルまで詳細に出力し、コードの挙動を細かく確認したいはずです。
一方、本番環境でDEBUGログまで垂れ流していては、ログの量が肥大化し、重要な情報が埋もれてしまいます。

そこで有効なのが、環境変数によってログレベルの閾値を切り替える設計です。

const logLevel = process.env.LOG_LEVEL ?? (process.env.NODE_ENV === "production" ? "info" : "debug");

このように、本番環境ではデフォルトでinfo以上のみを出力し、開発環境ではdebugまで出力する、という切り替えを行うのが定石です。
さらにステージング環境ではwarn以上に絞る、といった柔軟な運用も可能になります。

環境ごとにログレベルを固定値でハードコーディングするのではなく、外部から注入できる設計にしておくことで、障害発生時にレベルを一時的に引き下げ、詳細なログを取得するといった機動的な対応もできるようになります。
運用の柔軟性を確保するうえで、この切り替えの仕組みは欠かせない要素と言えるでしょう。

TypeScriptプロジェクトにおけるログレベル設計の実践指針

TypeScriptのロゴとログレベルの設定画面が並んだイメージ

前章では、ログレベルの基本的な意味と環境ごとの切り替え方を確認しました。
ここからは、実際のTypeScriptプロジェクトにおいて、どのようにログレベルを設計へ落とし込んでいくべきか、より実践的な指針を解説します。

業務ロジックとインフラ層で分けるログレベルの粒度

Webアプリケーションは、一般的にレイヤードアーキテクチャで構成されます。
コントローラー層、サービス層、リポジトリ層といった各レイヤーでは、求められるログの粒度が異なることに注意が必要です。

  • コントローラー層:リクエストの受付とレスポンスの返却をINFOで記録
  • サービス層:ビジネスロジックの分岐や重要な判断をDEBUGまたはINFOで記録
  • リポジトリ層:データベースへのクエリ発行やタイムアウトをDEBUGやWARNで記録
  • インフラ層:外部APIとの通信失敗やタイムアウトをERRORで記録

このように層ごとに役割を明確化しておくと、障害発生時にどのレイヤーで何が起きたのかを素早く切り分けられます。
すべてのログを一律にINFOで出力してしまうと、この切り分けができなくなってしまう点に注意しましょう。

また、TypeScriptの型システムを活用し、ログ出力用の関数にレイヤー情報を引数として持たせる設計も有効です。

interface LogContext {
  layer: "controller" | "service" | "repository" | "infra";
  operation: string;
}

function logInfo(message: string, context: LogContext): void {
  logger.info({ message, ...context });
}

型によってレイヤー名を制約しておけば、タイプミスによる分類漏れを防げます。
これは、動的型付け言語にはない、TypeScriptならではの恩恵と言えるでしょう。

過剰なログとログ不足のバランスの取り方

ログは多ければ良いというものではありません。
過剰なログは、ストレージコストの増大だけでなく、本当に必要な情報を見つけにくくするというデメリットをもたらします。
一方、ログが少なすぎれば、障害発生時に手がかりが得られず、原因究明が困難になります。

このバランスを取るための一つの指針として、「そのログは、後で誰かの意思決定に使われるか」を基準に考えることをおすすめします。
単に処理が通過したことを記録するだけのログは、本当に必要でしょうか。

判断に迷った際は、以下の観点で見直してみると良いでしょう。

  1. そのログは障害調査の際に参照される可能性があるか
  2. そのログはビジネス上の重要な意思決定に関わるか
  3. そのログを削除しても、他の手段で同等の情報を得られないか

これらに当てはまらないログは、思い切って削除するか、DEBUGレベルへ格下げすることを検討すべきです。
ログ設計は一度作って終わりではなく、運用しながら継続的に見直していくものだと捉えておくと良いでしょう。

JSON構造化ログとは?テキストログとの違いを比較

テキストログとJSON形式のログを左右に並べて比較する図

これまで、ログレベルの設計について解説してきました。
ここからは、もう一つの重要な要素である「ログの構造化」について掘り下げていきます。
構造化ログとは、ログをあらかじめ定義されたスキーマに沿ったデータ形式、多くの場合JSON形式で出力する手法を指します。

従来の自由記述テキストログと構造化ログの違いを、簡単な例で見比べてみましょう。

2026-08-18 10:23:45 [ERROR] User login failed for user_id=12345, reason=invalid_password

これに対し、構造化ログでは以下のような形式で出力します。

{"timestamp":"2026-08-18T10:23:45Z","level":"error","message":"User login failed","user_id":12345,"reason":"invalid_password"}

見た目の情報量はほぼ同じですが、この2つには決定的な違いがあります。
前者は人間が読むことを前提とした文字列であり、後者は機械が処理することを前提としたデータです。
この違いが、運用フェーズにおいて大きな差を生みます。

構造化ログのメリット

構造化ログを採用する最大のメリットは、検索性と集計性の飛躍的な向上にあります。

  • 特定のフィールド(user_idなど)を条件にした高速な検索が可能になる
  • ログ管理ツール上でグラフ化や集計を自動的に行える
  • ログのパースが不要になり、監視システムとの連携が容易になる
  • フィールドの構造が統一されるため、表記揺れによる検索漏れを防げる

例えば、特定のエンドポイントのレスポンスタイムだけを抽出して平均値を算出する、といった分析も、構造化されたフィールドがあれば数クリックで実現できます。
テキストログでは、このような分析のために正規表現を駆使した前処理が必要になり、手間と実行時間の両面でコストがかかります。

また、ログをそのままデータベースやログ管理サービスに投入できる点も見逃せません。
構造化ログは、単なる記録ではなく、活用可能なデータ資産として扱えるようになるのです。

構造化ログのデメリットと注意点

一方で、構造化ログにも注意すべき点があります。
まず、JSON形式は人間が目視で読む際の可読性がテキストログに劣ります。
開発中にターミナルで直接確認する場合、フィールドが多くなるほど視認性が低下してしまいます。

この点については、開発環境ではプリティプリント形式や整形ライブラリを併用し、本番環境では1行のJSONとして出力する、といった使い分けで対応するのが一般的です。

また、フィールドの命名規則を統一しておかないと、構造化された恩恵が半減してしまう点にも注意が必要です。
あるログではuser_id、別のログではuserIdといった表記揺れがあると、検索クエリが複雑化してしまいます。
プロジェクト全体でスキーマを定義し、チーム内で命名規則を共有しておくことが、構造化ログを効果的に運用するうえでの前提条件と言えるでしょう。

TypeScriptでJSON構造化ログを実装する方法

TypeScriptのエディタ画面にログライブラリのコードが表示されている様子

構造化ログの理論を理解したところで、ここからは実際にTypeScriptプロジェクトへ導入する方法を見ていきましょう。
ゼロから自作する選択肢もありますが、まずは実績のあるロギングライブラリを活用する方法から紹介します。

pinoを使った構造化ログの実装例

pinoは、高速な処理性能を売りにしたNode.js向けのロギングライブラリです。
JSON構造化ログをデフォルトで出力する設計になっており、TypeScriptとの相性も良好です。

まずはパッケージをインストールします。

npm install pino

基本的な使い方は以下の通りです。

import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
});

logger.info({ userId: 12345 }, "User logged in successfully");
logger.error({ err: new Error("DB connection failed") }, "Database error occurred");

第一引数にオブジェクトを渡すことで、そのフィールドがログのJSONにマージされます。
pinoの特筆すべき点は、非同期I/Oを前提とした設計により、他のロギングライブラリと比較してオーバーヘッドが小さいことです。
高頻度でログを出力するAPIサーバーにおいて、この性能差は無視できません。

winstonを使った構造化ログの実装例

winstonは、Node.jsのロギングライブラリとして長年利用されてきた実績があり、柔軟なカスタマイズ性を特徴とします。
出力フォーマットや出力先(トランスポート)を細かく制御したい場合に適しています。

npm install winston
import winston from "winston";

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL ?? "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [new winston.transports.Console()],
});

logger.warn("Unusual request pattern detected", { ip: "192.0.2.1" });

winston.format.json()を指定するだけで、出力形式をJSON構造化ログへ変更できます。
複数のトランスポートを組み合わせ、コンソールとファイル、外部サービスへ同時に出力するといった構成も容易に実現できる点が、winstonの強みです。

pinoとwinstonのどちらを選ぶべきか迷った場合は、以下の観点を参考にしてください。

観点 pino winston
実行速度 非常に高速 標準的
カスタマイズ性 シンプル志向 高い自由度
エコシステムの歴史 比較的新しい 長い実績がある

独自ロガーを自作する場合の設計ポイント

小規模なプロジェクトや、外部ライブラリへの依存を避けたい場合には、独自のロガーを自作する選択肢もあります。
ただし、自作する際には以下の設計ポイントを押さえておく必要があります。

  • ログレベルを型として定義し、タイプミスによる不正な値を防ぐ
  • タイムスタンプはISO 8601形式など、パースしやすい統一フォーマットに揃える
  • 出力先を抽象化し、コンソール出力とファイル出力を切り替えられる構造にする
  • エラーオブジェクトのスタックトレースを漏れなくシリアライズする

自作ロガーは、要件に合わせて最小限の実装で済む一方、非同期出力の最適化や例外処理の考慮など、既存ライブラリが既に解決している課題を自前で対応する必要があります。
プロジェクトの規模や要件を踏まえたうえで、既存ライブラリの採用と自作のどちらが適切か、慎重に判断することをおすすめします。

構造化ログに含めるべき必須フィールドの設計

JSONログのフィールド項目がリスト状に並んでいる図

構造化ログを導入する際、最も重要なのはどのライブラリを使うかよりも、どのようなフィールド設計をするかです。
フィールド設計が曖昧なままでは、せっかくJSON形式にしても、検索性や解析性を十分に引き出せません。
ここでは、実務で欠かせない必須フィールドを3つの観点から解説します。

timestamp・level・messageの基本構成

どのようなログであっても、最低限含めるべき基本フィールドが3つあります。

フィールド名 役割 推奨形式
timestamp ログが発生した日時 ISO 8601形式
level ログの重要度 debug/info/warn/error/fatal
message 何が起きたかの要約 簡潔な英語または日本語の文章

timestampは、必ずタイムゾーン情報を含んだISO 8601形式で記録すべきです。
サーバーのローカルタイムに依存した形式では、複数のリージョンにまたがるシステムでログを突き合わせる際に混乱が生じます。

levelは、これまで解説してきたログレベルの分類に沿って設定します。
messageは、あくまで人間が一目で状況を把握するための要約であり、詳細な情報は別のフィールドに切り出すのが望ましい設計です。

logger.error({ userId: 12345, orderId: "ORD-9876" }, "Failed to process payment");

このように、messageには簡潔な要約のみを記述し、詳細な文脈情報は追加のフィールドとして分離することで、検索性が大きく向上します。

traceIdやrequestIdによるリクエスト追跡

マイクロサービスや複数のレイヤーで構成されるWebアプリでは、1つのリクエストが複数のコンポーネントを経由することが珍しくありません。
このとき、各コンポーネントのログを個別に追っていては、リクエスト全体の流れを把握するのが困難です。

そこで有効なのが、リクエストごとに一意な識別子を発行し、すべてのログに埋め込む手法です。

  • requestId:1つのHTTPリクエストに対して一意に発行するID
  • traceId:複数のサービスをまたぐ処理全体を追跡するためのID
  • userId:操作を行ったユーザーを特定するためのID

これらのIDをリクエストの入口(ミドルウェアなど)で発行し、以降の処理すべてに伝播させることで、ログ管理ツール上でrequestIdをキーに検索するだけで、そのリクエストに関連するすべてのログを時系列で追跡できるようになります。

app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  next();
});

UUIDを用いた識別子の発行は、TypeScript製のWebアプリにおいて標準的な手法と言えるでしょう。

エラーオブジェクトのシリアライズ方法

JavaScriptのErrorオブジェクトは、そのままJSON.stringifyに渡しても、スタックトレースなどの重要な情報が失われてしまう性質を持ちます。
これは、Errorオブジェクトのプロパティの多くが列挙不可能(non-enumerable)に設定されているためです。

console.log(JSON.stringify(new Error("test")));
// => {} が出力されてしまう

この問題を回避するには、Errorオブジェクトを明示的に分解し、必要なプロパティを抽出してからログに含める必要があります。

function serializeError(err: unknown): Record<string, unknown> {
  if (err instanceof Error) {
    return { name: err.name, message: err.message, stack: err.stack };
  }
  return { message: String(err) };
}

多くのロギングライブラリには、この変換処理を自動で行うシリアライザ機能が組み込まれています。
自作する場合も、独自の例外クラスを利用しているのであれば、そのクラス固有のプロパティ(エラーコードなど)も漏れなく出力対象に含めておくことをおすすめします。

本番運用で活きるログ管理・監視ツールとの連携

クラウド上の監視ダッシュボードにログが集約されるイメージ

ここまで、TypeScriptアプリケーション内でのログ設計と実装を中心に解説してきました。
しかし、ログは出力して終わりではありません。
本番運用においては、出力したログをどのように収集・監視・分析するかという後工程こそが、実際の障害対応の効率を左右します。

CloudWatch LogsやDatadogとの連携方法

構造化ログの真価は、外部のログ管理・監視サービスと連携させたときに発揮されます。
代表的なサービスとしては、AWSのCloudWatch LogsやDatadogが挙げられます。

サービス 特徴 主な連携方法
CloudWatch Logs AWS環境との親和性が高い エージェント経由またはSDK直接送信
Datadog 高度な可視化とアラート機能 dd-traceやHTTP Intake API

CloudWatch Logsと連携する場合、EC2やECS上で稼働するアプリケーションであれば、CloudWatch Logsエージェントを介して標準出力のログを自動収集させる構成が一般的です。
コンテナ環境であれば、コンテナの標準出力にJSON形式でログを吐き出すだけで、収集の仕組みを別途実装する必要がなくなります。

logger.info({ requestId: req.requestId, path: req.path }, "Incoming request");

このように標準出力へ構造化ログを出力する設計にしておけば、収集基盤側の設定次第で柔軟に連携先を切り替えられます。
アプリケーションのコード自体が特定のクラウドサービスに依存しない、疎結合な構成を維持できる点は、設計上のメリットとして見逃せません。

Datadogを利用する場合は、専用のトレーシングライブラリを導入することで、ログとAPM(アプリケーションパフォーマンスモニタリング)のトレース情報を自動的に紐付けられます。
先述したtraceIdの仕組みと組み合わせることで、特定のリクエストにおけるレスポンスタイムの内訳まで、シームレスに追跡できるようになるでしょう。

ログのローテーションと保存コストの最適化

ログは時間の経過とともに際限なく蓄積されていきます。
何の対策もなければ、ストレージコストが右肩上がりに増加し続けることになりかねません。
ここで重要になるのが、ログのローテーションと保存期間の最適化です。

ログ保存における一般的な戦略としては、以下のようなアプローチが挙げられます。

  1. 直近のログは高速に検索できるホットストレージに保存する
  2. 一定期間を過ぎたログはコストの低いコールドストレージへ自動的に移行する
  3. 法令や社内規定で定められた保存期間を超えたログは自動的に削除する

例えばCloudWatch Logsであれば、ロググループごとに保存期間(Retention)を設定でき、期限を過ぎたログは自動的に削除されます。
Datadogのようなサービスでも、インデックス保持期間を調整することで、検索可能なログの範囲とコストのバランスを取ることが可能です。

重要なのは、すべてのログを同じ保存期間で一律に扱わないことです。
ERRORやFATALレベルのログは長期保存し、DEBUGレベルのログは短期間で削除するといった、レベルに応じた保存戦略を採ることで、コストを抑えつつ必要な情報は確実に残すという、両立の難しい要件を満たすことができます。

ログ設計・運用時によくある失敗と回避策

警告アイコンとチェックリストが並んだ失敗回避のイメージ

これまで、ログレベルの設計から構造化ログの実装、外部ツールとの連携まで一通り解説してきました。
最後に、実務でありがちな失敗パターンとその回避策を確認しておきましょう。
設計段階で見落としがちな落とし穴を知っておくことは、堅牢なログ基盤を構築するうえで欠かせません。

個人情報や機密情報のログ出力による情報漏洩

構造化ログはリクエストの内容をオブジェクトごと出力できる手軽さがある一方、意図せず機密情報を含めてしまうリスクをはらんでいます。
パスワードやAPIキー、クレジットカード情報といったデータが平文でログに残ってしまえば、情報漏洩の重大な原因になりかねません。

例えば、デバッグ目的でリクエストボディ全体をそのままログに出力するコードは、一見便利に見えますが危険です。

logger.debug({ body: req.body }, "Incoming request body");

このような実装は、フォームに含まれるパスワードフィールドがそのままログに記録されてしまう典型的なアンチパターンです。
回避策としては、以下のような対策が有効です。

  • ログ出力前にマスキング処理を行うミドルウェアを共通化する
  • パスワードやトークンなどのフィールド名をあらかじめ定義し、自動的に伏せ字へ変換する仕組みを導入する
  • リクエストボディ全体をそのまま出力する実装を、コードレビューで検出できるルールを設ける

多くのロギングライブラリには、指定したフィールドを自動的にマスクする機能が用意されています。
こうした機能を積極的に活用し、機密情報が誤って記録される可能性を仕組みとして排除しておくことが重要です。

パフォーマンス低下を招く同期的ログ出力

もう一つの見落としがちな失敗が、ログ出力処理がアプリケーション本体のパフォーマンスに与える影響です。
特に、ファイルへの書き込みや外部サービスへの送信を同期的に行っていると、リクエスト処理のたびにI/O待ちが発生し、レスポンスタイムの悪化を招きます。

高頻度でアクセスされるAPIエンドポイントにおいて、この遅延は無視できない規模になることがあります。
ログを出力するたびにディスクI/Oやネットワーク通信の完了を待つ設計は、スループットを大きく損なう要因です。

この問題を回避するには、以下のようなアプローチが有効です。

  1. ログの書き込みを非同期キューに一旦蓄積し、バックグラウンドでまとめて処理する
  2. 先述したpinoのように、非同期I/Oを前提として設計されたライブラリを採用する
  3. ログの送信先が一時的に利用できない場合でも、アプリケーション本体の処理を止めないバッファリング機構を用意する

ログは本来、アプリケーションの主処理を補助する脇役です。
その脇役の処理が原因で主処理が遅延するようでは、本末転倒と言わざるを得ません。
ログ出力自体の性能特性にも目を向け、非同期的な設計を選択することが、安定したWebアプリケーション運用の前提条件となります。

まとめ:適切なログレベルとJSON構造化で障害調査を効率化しよう

整理されたログとチェックマークが並ぶまとめのイメージ

ここまで、TypeScriptで構築するWebアプリにおいて「ログが追えない」問題がなぜ発生するのか、その原因から具体的な解決策までを一通り解説してきました。
最後に、本記事で扱った内容を振り返りながら、実践に移すための要点を整理しておきましょう。

そもそもの問題の発端は、console.logの無秩序な多用と、ログレベルという概念が設計されていない点にありました。
すべてのログが同列に扱われてしまえば、障害発生時に本当に重要な情報を見つけ出すことは困難です。
この課題に対しては、DEBUG・INFO・WARN・ERROR・FATALという5段階の基本的なログレベルを理解し、レイヤーごとの役割に応じて適切に使い分ける設計が有効でした。

  • 開発環境と本番環境で出力レベルの閾値を切り替える
  • コントローラー層・サービス層・インフラ層など、レイヤーごとにログの粒度を調整する
  • 「そのログが誰かの意思決定に使われるか」を基準に、過剰なログと不足を防ぐ

次に取り上げたのが、JSON構造化ログという考え方です。
テキストの羅列であった従来のログを、機械可読なデータ形式へと変換することで、検索性と集計性が飛躍的に向上することを確認しました。
pinoやwinstonといったライブラリを活用すれば、TypeScriptプロジェクトへの導入も難しくありません。

また、構造化ログを効果的に運用するためには、フィールド設計そのものが重要であることにも触れました。
以下の観点は、実装に着手する前にぜひ再確認しておきたいポイントです。

設計観点 押さえるべきポイント
基本フィールド timestampはISO 8601形式、levelとmessageを明確に分離する
追跡用フィールド requestIdやtraceIdでリクエストの流れを紐付ける
エラー情報 Errorオブジェクトを明示的にシリアライズし、スタックトレースを残す

さらに、出力したログを実際の運用でどう活かすかという観点から、CloudWatch LogsやDatadogといった外部ツールとの連携方法、そしてログのローテーションによる保存コストの最適化についても解説しました。
ログは出力して終わりではなく、収集・分析・保存というライフサイクル全体を設計してこそ、初めて価値を発揮するものです。

最後に、個人情報や機密情報の混入、同期的なログ出力によるパフォーマンス低下といった、実務でありがちな失敗パターンとその回避策も確認しました。
これらは、設計段階で意識しておかなければ、後から気づいて修正するのに大きなコストがかかる問題です。

ログは、システムが正常に稼働しているときには意識されにくい存在です。
しかし、いざ障害が発生したとき、ログの品質がそのまま復旧までの時間を左右します。
適切なログレベルの設計とJSON構造化という、地道ながらも本質的な取り組みを積み重ねることが、結果として障害対応のスピードと信頼性の向上につながるのです。

本記事で紹介した内容を参考に、まずは自身のプロジェクトで無秩序に使われているconsole.logを洗い出すところから始めてみてはいかがでしょうか。
小さな一歩の積み重ねが、運用に耐えうるログ基盤の構築へとつながっていくはずです。

コメント

タイトルとURLをコピーしました