Article by: Kyle Tryon
アプリケーションがちょっとした個人開発から、多くのユーザーに使われる複雑な分散システムへと成長していくにつれて、従来の console.log に頼ったデバッグ手法では通用しなくなります。本当に観測可能なシステムを構築するには単なるテキストログから、構造化されクエリ可能で、トレースに紐づくイベントへと移行していく必要があります。
要点:ログ戦略の転換
多くの人はログをパンくずのように扱い、各行が実行されたことを確認したり、デバッグのために出力結果を記録したりします。ところが本番環境では、そのパンくずはすぐにノイズの山になります。必要なのは処理の過程を逐一ログに残すやり方から、マイルストーンをログに残すやり方へ切り替えることです。
- ノイズを取り除く:ノイズを生み、クエリや相関が難しい「薄い」ログから離れましょう。
- 高カーディナリティを受け入れる: タスクの進行に伴って積み上がる「厚い」コンテキストをログに詰め込みましょう。ユーザーID、注文ID、カート情報などを含め、任意のイベントについて必要なデータをクエリできるようにします。
- 点をつなぐ: Sentry を使ってログをトレースに紐づけたままにし、各ログをそれを引き起こした特定のリクエストへ紐づけます。
ログの洪水:なぜ本番環境では console.log が通用しないのか
ログが集中管理されなくなり、時系列で追えなくなった瞬間に console.log は破綻します。複数のユーザーとサービスが同時に動く本番環境では、ログはすぐにさまざまなイベントが入り混じったストリームになってしまい、特定の1リクエストについて何が起きたのかを復元する明確な手がかりがなくなります。

関連するログ同士をつなぐ共通のトレースと、フィルタ可能で有用なデータがなければ、こうしたログは本番環境では実質的に役に立たなくなります。
LogTape と Sentry で本番品質のロギングを実装
Sentry はトレースに紐づくロギングを提供します。トレースを使えば、1つのリクエストに関する全体のコンテキスト(そのリクエストに紐づくログも含む)を確認できます。これにより特定の issue やリクエストに関連するトレースとログを簡単にクエリできるようになります。
さらに Sentry Logs には、属性や構造化データをもとにログを検索できる強力なクエリエンジンがあります。そこから検索結果に基づいてアラートやダッシュボードを作成することも可能です。

LogTape はあらゆる JavaScript ランタイム向けの軽量なロギングライブラリです。LogTape のようなロギングライブラリを使うと、コードに計測を組み込み、自動でリッチな構造化ログを出力できるようになります。また「log sink」を使って、それらのログを Sentry に送信できます。

構造化ログとは、単なる文字列ではなく、定義されたプロパティを持つ構造化オブジェクトとしてログを扱う形式です。
これにより本番環境でのデバッグにおいて、ログから必要なデータを見つけて可視化するための強力なクエリやフィルターを作成できます。

クイックスタート:Next.js セットアップ
この例では Next.js フレームワークを使いますが、ここで扱う考え方はどの JavaScript フレームワークにも応用できます。
まずは利用するフレームワーク向けのクイックスタートガイドに従って、プロジェクトで Sentry を初期化してください。Next.js の場合は、@sentry/wizard を使ってプロジェクトに Sentry を導入します。

ウィザードが完了すると、プロジェクト内にいくつかのファイルが作成されているはずです。これらによりエラー監視のための計測がプロジェクトに自動で組み込まれ、トレースとログの収集が始まります。
- instrumentation-client.ts — ブラウザで実行
- sentry.server.config.ts — Node.js で実行
- sentry.edge.config.ts — エッジランタイムで実行
instrumentation-client.ts は次のような内容になっているはずです。

また Sentry が正しく動作しているかをテストするための新しい /sentry-example-page ルートも追加されています。
開発サーバーを起動してそのサンプルページにアクセスし、「Throw error」ボタンをクリックすると、Sentry 側に issue が取り込まれ、関連するトレースも確認できるはずです。

このトレースは issue、セッションリプレイ、ログなど、1つのリクエストに関わるあらゆる情報を紐づけます。
LogTape を Sentry と連携
Sentry 側でログを受け取る準備はすでにできていますが、次に必要なのは 「そのログをどう送るか」 を定義することです。
LogTape で構造化ログを扱うには、「log sink」を活用します。ログをコンソールへ出す代わりに(または併用して)、トレースに紐づくログを Sentry に直接送信できます。
まず、LogTape と LogTape の Sentry 向けパッケージをインストールします。

クライアント側の設定
先ほど確認した instrumentation-client.ts に LogTape の設定を追加します。

残りの設定を入れたあとで分解して説明しますが、要するにここでは logger オブジェクトを使ってログを出すと コンソールと Sentry の両方に送られるように LogTape を設定しています。
また、ログにカテゴリを設定しています。これは Sentry 上で、ドメインごとにログをグルーピングするために使われます。
サーバー側の設定
LogTape には contexts という特別な機能があり、コールスタックに沿ってデータを渡せます。つまり任意のタイミングでコンテキスト情報をログのスタックに追加しておき、送信されるログすべてにその情報を含めることができます。
バックエンドでは 暗黙のコンテキスト(implicit contexts)を活用できます。これによりスタック内のすべてのログに対してデータを付与できます。下記設定を追加すると、withContext メソッドを使って 現在のスコープに自動でデータを挿入でき、以降のサブルーチンはその情報を自動的に継承します。
sentry.server.config.ts にサーバー向けの LogTape 設定を追加します。

暗黙的なコンテキスト継承(implicit context inheritance)を使い、アプリ全体でデータを半自動的に積み上げて収集していきます。そして最終的にログメッセージを出力するときには、その時点までのスタック全体のデバッグ情報が含まれるようになります。
Sentry と連携して Next.js アプリで LogTape を使う
Sentry と LogTape の設定ができたので、logger オブジェクトを使ってログメッセージを記録できるようになります。
getLogger を使って、指定したカテゴリに対応するルートロガーを取得します。このカテゴリはフロントエンドとバックエンドそれぞれで instrumentation-client.ts と sentry.server.config.ts に設定したものです。
ここでは追加で api というカテゴリも設定しています。これによりこのスコープ付きの子ロガーから出るログにはすべて category: nextapp-demo.api が付与されるため、あとからクエリで検索できます。
必要に応じて、ネストさせてさらに細かくスコープを切っていくこともできます。


Sentry の Explore > Logs では、このクエリは category が nextapp-demo.api を「含む」ログを検索しています。つまりこのクエリでは API のログがすべて表示され、.api.posts のように .api 配下にネストされたカテゴリのログも含まれます。
Sentry で LogTape のログをクエリ
フロントエンドとバックエンドからいくつかログを収集したら、Sentry の Log Explorer に移動します。

api のようなカテゴリを設定できるだけでなく、ネストしたカテゴリの配列を指定することもできました。さらにログと一緒に送られる構造化データと組み合わせることで、利用可能なあらゆる属性を条件にクエリを組み立てられるようになります。


logger で生成される各ログには、継承された属性が含まれます。これらはすべて、クエリ条件として利用できます。

戦略:何をいつログに記録するか
すべての関数に計測を入れる前に、まず方針が必要です。大量トラフィックの本番アプリは何百万件ものログを生みうるため、精査が難しい「ノイズ」につながり、保存容量の上限も消費してしまいます。
適切なレベルを選ぶ
ログレベルはノイズを減らすための最上位のフィルターとして使います。本番環境では Sentry sinkは通常 info または warn に設定し、ローカル作業向けにはconsole sink を debug のままにしておくのが一般的です。

- Debug: 高頻度で出るデータ(例:「PostItem ID: 123 をレンダリング中」)
- Info: 主要なライフサイクルイベント(例:「ユーザーログイン」「決済完了」)
- Warn: 復旧可能な問題(例:「API がタイムアウトしたため再試行」)
- Error: 直ちに対応が必要な重大障害
いつログに記録するか:イベント駆動のアプローチ
シグナル・ノイズ比を健全に保つために、すべてのコード行をログに記録するのではありません。代わりに、状態の遷移をログします。
- 「ハッピーパス」の境界: 大きな処理の開始と終了をログに記録します(例:
checkout_started→checkout_completed)。 - 外部依存: 外部依存先の呼び出しは失敗する可能性が高くなります。外部依存を呼ぶ前に、将来の障害調査で役立つようにリクエストペイロードのデータを含めたログを追加しておくとよいでしょう。
- 復旧可能なエラー(Warn): アプリは壊れていないものの「通常ではない」事象(例:キャッシュミスにより重いDB再構築が必要になった)には
warnを使います。こうしたログのクエリは、パフォーマンス監視やアラート作成に特に役立ちます。
ログは少なく、カーディナリティは高く
よくある落とし穴はログのサイズが膨らんだりコストが増えたりしそうだからと、ログにデータを「詰め込む」ことを避けてしまうことです。ですが実際には、バグを再現するために必要な特定のデータが入っていなければ、ログが1000行あっても役に立ちません。
カーディナリティとは、ログ内データのユニークさを指します。高カーディナリティなデータには、userId、sessionId、orderId、requestId のようなものがあります。昔の自前のロギング基盤は高カーディナリティ(クエリが遅くなる原因)を苦手としていましたが、Sentry のような最新のツールはむしろそれを前提に強みを発揮します。
変化のポイント:「おしゃべり」から「コンテキスト重視」へ
実行の各行を逐一ログするやり方から離れ、積み上げたコンテキストを伴うマイルストーンをログする方向へ移行したい、ということです。
たとえばカートのチェックアウトフローを考えてみましょう。「従来のやり方」だと、複数の個別ログ行を出すことになりがちです。
「おしゃべり」なトレース(分散しているが薄い)

複数の「薄い」ログがあり、それぞれはトレースに紐づいているものの、ログ内のデータをまとめて簡単にクエリできません。orderId や totalAmount を確認したい場合は、トレースをクリックして辿り、「Payment processed」のログを見つけ、そこに開発者がIDを入れてくれていることを祈る必要があります。
「高カーディナリティ」なイベント(情報が豊富ですぐに使える)
薄いログからトレースで物語を「つなぎ合わせる」ことに頼るのではなく、積み上げたコンテキストを伴うマイルストーンをログします。

トレースがあっても、高カーディナリティなログのほうが優れている理由は主に2つあります。
全体を横断して検索できる
トレースは特定の1リクエストをデバッグするのに役立ちます。一方で高カーディナリティなログは、システム全体をデバッグするのに役立ちます。Sentry の Log Explorer で、たとえば次のように問いかけられます。
「過去24時間の ‘Purchase Completed’ イベントのうち、discountCodeが ‘SAVE20’ で、かつlatencyMsが 2000 を超えるものをすべて表示して」
もし割引コードがトレースの3つ前の別ログ行に埋もれていたら、こうした検索はできません。認知負荷が下がる
Sentry で issue を開いたとき、関連する ID や状態がすべてまとまった「厚い」ログ1件を見られるほうが、ユーザーが何をしていたのかを把握するために「薄い」ログ20件を追いかけて組み立てるより、はるかに速いです。
高カーディナリティな「イベント駆動」のログに焦点を当てることで、ログは単なる診断の足跡から、強力な社内向け分析エンジンへと変わります。
React での LogTape の実践的な使い方
バックエンドとは異なり、ブラウザには暗黙的コンテキストのための AsyncLocalStorage API がありません。ユーザーデータをすべてのログに手動で付けるのを避けるために、React Context を使って継承を自動化できます。
では、React Context を使ってプロジェクトをどう構成すれば、ログに有用なデータをコンテキストに応じて自動で付与できるのかを見ていきましょう。
logger context を作成する
lib/logger-context.tsx(または context provider を定義している場所)にこのファイルを作成します。

次に、アプリ全体をその Provider でラップします。通常は layout(layouts)で行います。

これですべてのコンポーネントがユーザーコンテキスト付きの logger を自動的に受け取れるようになります。

手動で設定しなくても、すべてのログに { user: { id, email, name } } が自動で含まれます!
.LOG(まとめ)
- 構造化データ > 文字列: オブジェクトでログを書き、検索できるようにする。
- ネストによる整理: カテゴリをネストして、スタックの上下を簡単にクエリできるようにする。
- sink 戦略: フィルタ付きのsink を活用し、送りたいデータだけを送りたい先へ送る。
- コンテキストこそ重要:
AsyncLocalStorage(サーバー)と React Context(クライアント)で、ログ記述の繰り返しを避けつつ、必要なデータが必ず含まれるようにする。 - トレーサビリティ: トレース経由で、単一リクエスト内のすべてのログを簡単に見つけられる。
- 高カーディナリティ: イベントの終端で高カーディナリティなログを使い、クエリからリッチなメトリクスを作れるようにする。
- 監視: アラートとカスタムダッシュボードで重要指標を継続的に監視する。
LogTape と構造化ロギングについては、LogTape のマニュアルで詳しく記載しています。Sentry Logs については インタラクティブなデモをご参照下さい。
Original Page: Trace-connected structured logging with LogTape and Sentry
IchizokuはSentryと提携し、日本でSentry製品の導入支援、テクニカルサポート、ベストプラクティスの共有を行なっています。Ichizokuが提供するSentryの日本語サイトについてはこちらをご覧ください。またご導入についての相談はこちらのフォームからお気軽にお問い合わせください。




