オブザーバビリティ
今回は、オブザーバビリティについて書いてみたい。
私は会社で長くSentryを使ってきた。issueが上がればstack traceを開き、releaseとtagで範囲を絞り、再現条件を探す作業には慣れている。ところが、このブログにはerror monitoringがなかった。個人ブログで扱ってきたのはGoogle系のツールだった。Analyticsで訪問者を見て、Search Consoleでどの検索語から流入したかを確認し、それに合わせてタイトルや説明を直す。つまり、ユーザーを見るツールはあったが、serverがどのように失敗するかを見るツールはなかった。
ツールを知らなかったわけではない。先延ばしにした理由は別にあった。会社のプロジェクトでは、すでに誰かが敷いたinstrumentationの上に載ればよかったが、このブログでは最初から決める必要があった。どのツールを使うか、serverとブラウザのどちらに入れるか、何を失敗とみなすか。すべて設計上の判断であり、判断するにはこのブログがどう動くのかを改めて見直さなければならなかった。そのハードルに差しかかるたび、後回しにしていた。
その後、AIエージェントとこの作業を始めると、1日で終わった。マージしたPRが4件、検証用に作ってマージせず閉じたPRが2件だった。だが、この文章を書くきっかけは「早く終わった」ことではない。導入して測ってみると、自分が知っているつもりだったことのいくつかが間違っていたという事実だ。
このブログは問題なく動いていると思っていた。レスポンスは200で、ページもきちんと表示された。しかしinstrumentationを入れてみると、訪問者統計はすでに静かに空になっており、その手前でserverが1分以上ぶら下がっていた。オブザーバビリティのツールを導入する価値は、ツールを付けたこと自体にはなかった。付けなければ知る手段がなかったことを知れた点にあった。
そこで、この文章は二つの道を進む。一つはサービスの安定性、もう一つはユーザーの理解だ。どちらも最後には同じ話にたどり着く。
まず「オブザーバビリティ」という言葉を整理する
monitoringとobservabilityは混同されがちだが、指しているものは異なる。
Honeycombの創業者で、この分野の議論を長く主導してきたCharity Majorsは、自身のブログでmonitoringを、あらかじめ確認項目を決めてしきい値を設ける方法だと説明する。CPUが90%を超えたら通知、エラー率が1%を超えたら通知、といったものだ。一方、observabilityは次のように定義される。
外側から問いを投げかけるだけで、システム内部で何が起きているか、システムが陥りうるあらゆる内部状態を理解できるだろうか?
can you understand what is happening inside the system — can you understand ANY internal state the system may get itself into, simply by asking questions from the outside?
外から問いを投げかけ、システムが陥りうる任意の内部状態を理解できるか。鍵は「任意」だ。monitoringは事前に質問を決めておくもので、observabilityは事前に決めていない質問にも答えられる状態である。Majorsは別の記事で、この差をknown-unknownsとunknown-unknownsとして整理している。知っていることのうち分からないものと、分からないことすら知らないものの違いだ。
私が経験したのは、まさに後者だった。「GA呼び出しが失敗したらどうなるのか?」という問いを事前に投げていなかった。思いつきもしなかった。
もう一つ付け加えると、observabilityを「ログ、メトリクス、traceの三本柱」と紹介する説明をよく見るが、Majors本人は複数の記事でこの枠組みを批判的に扱ってきた。OpenTelemetryの公式文書もpillarではなくsignalという表現を使う。三つをすべて集めればobservabilityが得られると考えると、ツールはそろっているのに問いに答えられない状態になりやすい。(私も最初はツールの一覧から入り、この地点で方向を修正した。)
オブザーバビリティのツールは扱いがとても難しい
では、ツールの導入では具体的に何が難しかったのか。先延ばしにしてきた理由を振り返ると、難しさは二種類あった。
一つ目は、ブラウザが値をどのように作るかを知る必要があることだ。フロントエンドでユーザー側を観測するときは特にそうだ。たとえば訪問者が体感する性能を直接測るには、まずPerformanceObserverを理解しなければならない。しかし、このAPIにはオプションの組み合わせに制約がある。MDNによると、過去の項目まで受け取るbufferedオプションはtypeでしか使えず、entryTypesとは併用できない。スクリプトをページの最上部に置かなければ、初期指標を丸ごと逃すかどうかがここで決まる。
指標の定義自体も直感とは違う。レイアウトのずれを測るCLSは、ページで起きたすべての移動を合計した値ではない。web.devのCLS文書は、セッションウィンドウのうち最大のまとまり一つとして定義する。移動の間隔が1秒未満なら同じまとまりとし、一つのまとまりは最長5秒だ。さらに、ユーザー入力から500ミリ秒以内に起きた移動はhadRecentInputが付き、除外される。ユーザーがボタンを押してアコーディオンが開くのは不具合ではないからだ。この規則を知らずに自分で合算すると値が変わるが、どこが間違ったのかは分かりにくい。
指標の構成そのものも変わる。入力遅延を測っていたFIDは2024年3月12日からINPに置き換えられた。一つの操作の最初の反応だけを見る指標から、ページの生存期間全体にわたる操作の応答性を見る指標へ変わったのだ。
もちろん、この計算を自分でする必要はない。Google製のweb-vitalsライブラリを使えばよい。ただしその文書には、ライブラリを使っても残る落とし穴が整理されている。これらのAPIはiframeの中を見られないため、iframeを使うページでは、ライブラリの測定値とChromeユーザーエクスペリエンスレポート(CrUX)の値が分かれる。バックグラウンドタブで読み込まれたページでは、CLS、FCP、LCPが一切報告されない。back/forward cacheから復元されると指標は再度報告される。したがって値が予想と違うとき、サイトが遅いのか測定規則によるのかを見分けるには、結局ブラウザの知識が必要になる。
二つ目は、instrumentationをどこに入れるか判断することだ。この区別はOpenTelemetryの文書が明確に説明している。ソースを修正せずエージェントとして取り付ける方法をzero-code instrumentationと呼び、その範囲を次のように説明する。
通常、zero-code instrumentationは、使用しているライブラリにinstrumentationを追加します。つまり、リクエストとレスポンス、データベース呼び出し、メッセージキュー呼び出しなどがinstrumentationの対象になります。一方、アプリケーションコードは通常、対象になりません。コードにinstrumentationを施すには、code-based instrumentationを使用する必要があります。
Typically, zero-code instrumentation adds instrumentation for the libraries you're using. This means that requests and responses, database calls, message queue calls, and so forth are what are instrumented. Your application's code, however, is not typically instrumented. To instrument your code, you'll need to use code-based instrumentation.
自動instrumentationが無料で教えてくれるのは、ライブラリの境界だ。HTTPリクエストが入り、DB呼び出しが出たという事実までである。**アプリケーションコードがどのような判断をしたかは、通常教えてくれない。**それを知るには手で入れなければならない。
そして、私に必要だったのはまさに後者だった。「GA clientが呼ばれた」ではなく、「このブログの統計取得関数が失敗を飲み込み、既定値を返した」と知る必要があった。
AIを使って変わったこと
では、AIは何を変えたのか。先に断っておくと、私には比較対象がない。同じ作業を一人で行ったことがないため、「1日」という数字は成果ではなく、着手のハードルが下がったという話としてだけ読むべきだ。それを踏まえると、変化は二つある。
一つは、上記の規則をすべて暗記していなくてもよいことだ。以前は値がおかしいとき、自分のコードのせいか測定規則のせいかを切り分けるだけで半日かかった。今は観測した値をそのまま持ち込み、「この条件ではこの指標がどう計算されるか」を文書と突き合わせて絞り込める。もちろん、うのみにしてはいけない。この文章を書きながらも、AIが論文にない文をもっともらしく作るのを一度見つけた。そのため根拠に使う文はすべて原文で確認した。ただし、どこを確認すべきか知ることと、すべてを暗記することは別問題であり、後者の負担は確実に減った。
もう一つは、instrumentationの候補地点を一緒に見て回ることだ。候補を並べ、なぜそこなのか根拠を交わす過程は、一人のときより速かった。ただし、**何を失敗とみなすかという判断は最後まで私の仕事だった。**以下は、まさにその判断が誤っていた記録である。
失敗したのに成功として報告されていた
最初の計画は単純だった。統計APIのroute handlerにあるcatchへエラー報告を追加する。そうすればGA呼び出しの失敗が分かる。合理的に思えた。
ところが、ローカルのproduction buildに誤ったサービスアカウントキーを注入して意図的に失敗させると、**エラーはrouteのcatchに届かなかった。**一層下の統計取得モジュールにある四つのcatchブロックが先に捕捉し、既定値を返していた。その結果、レスポンスは次のようになった。
HTTP 200 OK
{ "slug": "/260610", "views": 0 }
訪問者には統計が0と見え、serverは正常だと答える。instrumentationがなければ知る方法がない。(この階層構造はエラー処理で扱った。当時は「どこで捕捉すべきか」を論じたが、今回は「捕捉したのに誰も知らない」に出会った。)
そこでinstrumentationの位置をrouteではなくその四か所へ移し、どのクエリで起きたかを区別できるtagを付けた。このtagが後に決定的な役割を果たす。
観測の非対称性
私はこれを「失敗が失敗に見えない状況」程度に呼んでいたが、調べるとすでに正確な名前があった。MicrosoftとAzureのチームが2017年のHotOSで発表した論文Gray Failure: The Achilles' Heel of Cloud-Scale Systemsである。
論文によると、クラウドの大規模な可用性事故は通常、完全停止ではない。部品は正しく動くか完全に止まるかのどちらかだとする単純な失敗モデルに基づく復旧機構は、こうした状況には不適切で、ときに悪化させる。そして中心的な特徴を次のように定める。
gray failureの重要な特徴はdifferential observabilityであると私たちは主張する。つまり、アプリケーションが被害を受けていても、システムの障害検出器が問題に気づかない場合がある。
we argue that a key feature of gray failure is differential observability: that the system's failure detectors may not notice problems even when applications are afflicted by them.
differential observability、すなわち観測の非対称性だ。一方の主体は失敗によって被害を受けているのに、別の主体はその失敗を認識できず、しかも後者こそが障害検出と復旧を担っている。論文の例が印象的だった。リクエスト処理モジュールが停止してもheartbeatモジュールが生きていれば、heartbeatに依存するエラー処理モジュールはシステムを健全と判断し、サービスを要求したclientは失敗と判断する。
論文は解決の方向も示す。異なる構成要素が何を失敗とみなすか、その認識の隔たりを埋めることに集中すべきだという。instrumentationの位置をrouteから下層へ移した作業は、まさにその隔たりを埋めるものだった。
そして実際の障害を捕捉した
instrumentationを入れた後、最初に上がった実際のproduction issueがこの話の次の場面だ。
GA呼び出しが65.877秒後にDEADLINE_EXCEEDEDで失敗していた。しかし前述の構造により、レスポンスは依然200だった。ホームは動的renderingで、統計領域をstreamingするため、ページ自体はすぐ表示される。その代わり、その場所が1分以上loadingのまま残り、静かに0で埋まる。
原因を追うと、使用しているGA clientライブラリの設定ファイルに次の記述があった。
"RunReport": { "timeout_millis": 60000, "retry_params_name": "default" }ライブラリ既定のRPC timeoutは60秒だ。そして私のコードは五つの呼び出し地点のどこでもtimeoutを上書きしていなかった。当時、観測された65.877秒を、60秒に接続とload balancerのオーバーヘッドが加わった値だと読んだ。(この解釈は後に揺らぐ。詳しくは後述する。)
ここで学んだのは、これは私だけのミスではなく、広く警告されてきた種類のミスだということだ。Google SREのGráinne SheerinによるgRPC公式ブログのdeadline記事は、タイトル下の一行目が「TL;DR: Always set a deadline」である。deadlineがなければ処理中の全リクエストが最大timeoutまでリソースを保持し、その結果メモリ枯渇、遅延増加、最悪の場合はprocessの停止を招くと説明する。私が使うGA clientもgRPCベースだ。同じ原則を文書はすでに警告していたが、呼び出し地点で守っていなかった。
修正はtimeoutを5秒に固定し、五つの呼び出し地点すべてに渡すことだった。そして応答しないローカルTCP serverを立て、決定的に再現した。
| 条件 | 経過時間 | エラーメッセージ |
|---|---|---|
| timeout未指定(修正前) | 60.04秒 | Deadline exceeded after 60.000s |
timeout: 5000(修正後) |
5.00秒 | Deadline exceeded after 5.000s |
数値は説明どおり正確に動いた。この表を得て初めて、timeout設定が実際にコードへ届くところまでは確認したと言えるようになった。(それまでは「timeoutがないからだろう」という推測だった。ただし、これがproductionの原因まで確認したわけではないことが後で分かる。)
もう一つ。5という数字自体に根拠はない。GAが正常なときの応答遅延分布を測っていないため、5秒は実質的に任意だ。ただし判断の方向には根拠があった。Google SRE本は第3章 Embracing Riskで、100%は決して正しい信頼性目標ではないと言う。達成不能なだけでなく、多くの場合ユーザーが望み、または気づく水準を超えた信頼性だからだ。このブログで訪問者数は補助情報である。正確に取得するより、早く諦めて既定値を描画する方が訪問者体験に良い。信頼性目標を100%にしないという決定だった。
直したと思った後でもう一度測った
ここまでが、本来この文章の結末になる予定だった。原因を見つけ、再現し、直したからだ。
ところが執筆中、習慣的にissue一覧を開き直した。修正commitがdeployされたreleaseに、同系列のDEADLINE_EXCEEDEDが100件以上積まれていた。最新のものは数時間前だった。
直近100件を抽出し、報告時間の分布を見た。一つ明確にしたいのは、この値はGAが実際に応答に費やした時間ではないという点だ。deadlineを設定した瞬間から、そのtimerが実際に発火した瞬間までのwall-clock time(実経過時間)である。この区別が後で重要になる。

読み方はこうだ。**下限は守られた。**5秒未満で切れたものは一件もなく、最短は5.16秒だった。上限を上書きしなかったときの再現値60秒と比べれば、5秒設定そのものはコードに届いている。しかし上は8分24秒まで伸び、中央値は61秒だ。さらに奇妙なのは、値がどの区間にも集中していないことだ。実際にGAが遅いことで生じた遅延なら上限付近に集まるはずだが、そうではない。
tagからさらに多くが分かった。100件のtagはstatsとpopularだけで、概ね二件一組で上がる。この二つの経路には共通点がある。**どちらも1時間cacheの後ろにある再検証経路だ。**一方、cacheなしでリクエストを受け、その場でGAを呼ぶ残り二つの経路(page、pages)は100件に一度も現れない。
これは重要だ。失敗が訪問者のリクエスト処理中ではなく、レスポンス完了後にcacheを再充填する作業でのみ起きていることを意味する。
そして、この観察は前節で書いた一文を揺るがす。統計の場所が1分以上loadingのままだと書いたが、失敗がレスポンス後の経路だけで起きるなら、訪問者はその時間を待っていない可能性がある。cacheが空の最初のリクエストでは事情が違うが、現在のデータではどちらかを区別できない。測らずに書いた文がもう一つあったわけだ。
もう一つ気になる。前節で65.877秒を60秒timeoutにオーバーヘッドが加わった値だと読んだが、そのイベントのオーバーヘッド項目を開き直すと、合計約2ミリ秒にすぎなかった。つまり、その時の解釈も今見ると根拠が弱い。同種の膨張が当時もあった可能性がある。
現時点の仮説はこうだ。このブログはserverless関数上で動く。serverless関数はレスポンス送信後、次の呼び出しまで実行環境が凍結される。その間timerも止まり、関数が目覚めた時に遅れて発火するなら、実際に待った時間ではなくwall-clock timeを基準に膨らんだ値が記録されうる。下限が正確に5秒に接していることも、上側の値がどこにも集中しないことも説明できる。失敗がレスポンス後の作業だけで出るという先の観察とも合う。
ただし、ここでもう一度注意しなければならない。**分布が仮説と矛盾しないことと、仮説を支持することは違う。**timerが遅れて発火する場面はいくつもある。serverlessの凍結以外にも、重いrenderingがevent loopを占有した可能性や、containerがCPUを絞った可能性もある。三つとも今回と同じ形の分布を作るため、このグラフは候補を絞れない。
別の説明も候補に残している。ライブラリ設定では再試行を含む総予算が600秒で、観測上の最大504秒はその内側に入る。ただし、このmethodは再試行対象codeの一覧が空なので、その経路は通らないと読める。どちらにせよ、**まだ確認できていない仮説だ。**検証方法を選ぶ際にも落とし穴があった。最初に思いついたのは呼び出し直前と直後の時刻を測ることだったが、それでは答えが出ない。関数が凍っている間もwall-clock timeは進むため、すでにある数値を再現するだけだ。可能性を分けるのは、同じ区間のCPU使用時間である。wall-clock timeで61秒が過ぎてもCPU時間がほぼ0なら、その時間は待っていたのではなく止まっていた。次の作業はそれになりそうだ。
この節をあえて残したのには理由がある。私は問題を直したと思っていた。再現し、表まで作ったので確実だと考えた。しかし開き直すと、そうではなかった。instrumentationを付けるのが一度の作業なら、observabilityは測り続けることだ。両者を同じものと勘違いすると、私と同じ誤りを犯す。
付け加えると、上のissue一覧、tag分布、時間の値はdashboardを開いて見たのではなく、エージェントに尋ねて得た。Sentryは公式MCP serverを提供しており、接続すればエディターからissueやイベントをそのまま照会できる。コードを見ていた場所にproduction issueを並べる作業が、何度ものクリックなしで可能になった。instrumentationを付けるコストだけでなく、蓄積データを見るコストも下がった。
付けることと使うことは別である
ここまで来て、もう一つ行った。ツールをどこまで使えるのか知るため、文書を最初から読み直した。今回の調査で決定的だったのは結局一つのtagで、それはinstrumentationを付ける際に何気なく追加したものだった。何気ないtagにそれだけの価値があるなら、意識して有効にする機能は何に答えられるのか。

releaseとcommitが第一の層だ。このブログはdeploy commit hashをreleaseとして付けているため、どのdeployから問題が始まったかは分かる。さらにreleaseへcommit一覧も上げれば、suspect commitsが動く。stack traceの各application frameについて、そのファイルと行番号のblame情報を確認し、最新commitが1年以内なら容疑として挙げる仕組みだ。そして、そのcommitの作成者を担当者として提案し、自動割り当てもする。commitをreleaseに関連付けると、commit messageに記載したissue IDによって、そのreleaseでissueが解決済みと表示される場合もある。このブログはsource mapをすでにアップロードしているため半分は整っているが、commitの関連付けはしていなかった。
所有ルールは個人ブログでは使う機会がないが、構造が興味深い。文書を見ると、ファイルpath、module、request URL、特定tagの値をUnix globでmatchingし、担当者やチームを指定する。たとえばpath:src/api/*はbackendチーム、という具合だ。初めて見たとき、これは通知のrouting機能ではなく、所有権をコードで記述する仕組みだと思った。issueが上がるたび誰が見るか人が決めるなら、忙しい日には誰も見ない。
tracingは問いの性質が違う。Sentry文書はtraceをアプリケーションから生じた関連イベントと処理の記録、spanを名前と時間が付いた一つの処理と定義する。一つのリクエストを複数のサービス、データベース、関数にまたがって追い、各区間がどれだけ時間を使ったかを見る。エラー報告が「何が壊れたか」に答えるなら、tracingは「時間がどこで消えたか」に答える。前節で詰まった地点はまさに後者だった。報告された経過時間が実際の待ち時間かを見分けるには、呼び出し区間の始点と終点が必要だ。このブログでは費用節約のためtraceのsamplingを10%だけにしたが、ここではその判断が惜しかった。
そして予約作業monitorが、この文章の主張に最も合うカードだ。エラー報告は起きたことだけを捕捉する。起きなかったことは捕捉できない。Cron monitorは作業開始時に進行中と知らせ、完了時に成功または失敗を知らせる。重要なのは第三の状態だ。文書は予定時刻にsignalが来なかった場合をmissed runとして別に分類する。schedulerの設定ミスや、作業がまったく開始できなかった場合がここに入る。
これは他人事ではなかった。このブログは毎週月曜日にSearch Consoleデータを自動収集する。後述するobservabilityの一層全体が、その作業の上に載っている。だが、ある週にその作業が静かに動かなくても、今は知る方法がない。失敗したのではなく、何も起きなかったのでエラーが出ないからだ。観測データを集める仕組み自体に死角があった。
まとめると、ツールを付ける作業は1日で済んでも、答えられる問いを広げる作業は残り続ける。どの層を有効にするかは、機能一覧を眺めても決まらない。**何を失敗とみなすかを先に決めてこそ、必要な層が分かる。**このブログでは、「週次収集が動かなかったこと」を失敗と認めた瞬間、有効にすべき層が一つ増えた。
文書が示唆した働きをオプションがしないとき
脇道として、もう一つ記録したい事例がある。性質は少し異なる。
source mapをアップロードした後、build出力から.mapファイルを削除する必要があった。容量が理由だ。server source mapは57MBで、server JS(15MB)より大きく、残すとdeploy関数のbundleへすべて入る。ちょうどアップロード後にsource mapを削除するオプションがあったので有効にした。
しかし実測すると、アップロード直後にもserverの.mapファイル57MBがそのまま残っていた。そのオプションは静的出力directoryだけを削除し、容量を占めるserver directoryには触れなかった。結局、削除対象pathを直接指定する方式に変えた。
同じ理由でアップロードログも条件付きで残すようにした。ログを常に切ると、token期限切れでアップロード全体が失敗しても静かに通り過ぎ、次に読めないstack traceを見るまで誰も気づかない。
これは失敗検出の問題ではなく、名前と実際の動作範囲のずれなのでgray failureには含めない。ただし教訓は同じ方向を指す。文書を読みオプションを有効にすることと、そのオプションが期待した働きをしたか確認することは別である。
エラーだけが観測対象ではない
ここまでが安定性の話だ。しかし観測情報を適切に扱う効用は、障害の捕捉で終わらない。冒頭で述べたもう一つの道、ユーザーを理解する側へ移ろう。
OpenTelemetry文書の信頼性の定義が、この転換をよく捉えている。信頼性とは、「サービスがユーザーの期待する働きをしているか」に答えるものだ。基準はserver指標ではなく、ユーザーの期待である。ならば、ユーザーが実際に何を経験しているかも測らなければならない。
このブログは結果として三層で観測している。

各層が答える問いは異なる。第一層は何が壊れたか、第二層は訪問者がどれだけ待ったか、第三層はそもそもどの検索語から来たかに答える。
第二層では重要な判断が一つあった。性能指標を測る方法には、管理された環境でページを開く方法と、実際の全訪問者を観測する方法がある。web.devは前者をlab data、後者をfield dataと呼び、両者の違いをまとめた文書で、両方あるならfield dataで優先順位を決めるよう勧める。実際のユーザー体験を代表するからだ。Lighthouseの点数が良くても、実際の訪問者の分布は異なりうる。そのため、このブログは点数測定で止まらず、実ユーザーの値をGA4へ送る。
基準はweb.devのWeb Vitals文書が示す。LCPは2.5秒以内、INPは200ミリ秒以下、CLSは0.1以下で、ページ読み込みの75percentileをmobileとdesktopに分けて判断する。平均ではなく、遅い側25%が超える境界を見るという意味だ。(この基準を理解して初めて、平均では遅いユーザーが丸ごと消えると気づいた。)
順位は下がったのにクリックは増えた
第三層の検索データでは、予想がもう一度覆った。
このブログはSearch Consoleデータを定期収集し、直近28日とその前の28日を比較する。蓄積データの中で、古い記事が一つ目に留まった。

表示回数は11%減り、平均順位は8.9位から11.6位に下がった。この二指標だけなら悪化した記事だ。しかしクリックは2回から13回に増え、クリック率は0.87%から6.37%になった。
まず少し盛り下げておくのが正直だろう。このpattern自体は検索データを扱った人には珍しくない。平均順位は表示回数で重み付けされた平均なので、上位に表示されても誰も押さなかった表示が消えると平均順位は悪化し、クリック率は機械的に上がる。検索語の構成が変わっただけでも逆転に見える。そしてクリック増加の絶対数は28日間で11回、小さな数字だ。
それを考慮しても残るものがある。実際にこの記事へ来た人は増え、表示回数と順位だけを見ていたら分からなかった。この数値から得たのは特定記事への結論ではない。何を指標にするかで結論が逆転するという事実だ。順位を成果とすればこの記事は修正対象で、クリックを成果とすれば成功だ。前節の信頼性の定義と同じ話である。基準をユーザー側に置くと、見えるものが変わる。
このブログで長く直してきたのはタイトルと説明だった。検索結果に出る文を実際の検索語に合わせて書き直す作業だ。この数字はその作業の効果を証明しない。順位が落ちてクリック率が上がった背景には、季節性や検索語構成の変化など、ほかの要因もいくらでもありうる。ただし、測らなければ、この方向の変化があったことすら知らなかった。
79KBを諦めた決定
三層の話で外せないのが、ブラウザinstrumentationを入れなかった決定だ。
client error monitoringも有効にしたかった。ブラウザだけで起きるエラーが見えない状態が気になり続けていた。そこで有効にし、bundleを実測した。clean buildでclient JSのgzip合計を比較した結果だ。
| 構成 | client JS (gzip) | 増加量 |
|---|---|---|
| 未適用 | 181.6 KB | 基準 |
| server専用(現在) | 182.3 KB | +0.7 KB |
| clientを含む | 260.4 KB | +78.8 KB |
server instrumentationは実質無料だが、ブラウザinstrumentationは78.8KBを要求した。bundle最適化オプションも試したが、数値は同じだった。client側のコストを減らす方法は、ブラウザ初期化ファイルを置かないことだけだった。
今見ると、この測定にも欠点がある。オプションを有効にしたのに1byteも減らないのは、オプションが効かなかった合図かもしれないが、当時は結論に都合よく読んだ。静的出力全体を合算した値であり、一人の訪問者が実際にdownloadする量とも違う。正確には「ブラウザobservabilityは79KB」ではなく、**「私の設定では、それより下げられなかった」**と書くべきだ。
このブログの導入目的は、serverで静かに失敗する呼び出しを捕捉することで、その部分は無料だった。そして、このブログでは読み込み性能はユーザー体験であり、検索表示の前提でもある。だから諦めた。面白いのは、この決定が前節の論理につながることだ。**ユーザーの期待を基準に信頼性を定義すると、observabilityを細かくすることが常に正しいとは限らない。**observability自体がユーザー体験を損ねうるからだ。
だが、こう書くと、先ほどtraceのsamplingを10%へ減らしたことを惜しんだのと矛盾して見える。どちらも費用のためobservabilityを減らした判断なのに、一つは後悔し、一つは正しかったというのか。後から整理した基準は、その費用を誰が払うかだ。trace samplingを減らして節約したのは私の料金で、ブラウザinstrumentationによる78.8KB増加は訪問者のデータと時間である。自分が払う費用なら多めに買う方が概ねよく、ユーザーが払うなら、その観測がユーザーに何を返すのか問うべきだ。
何に通知を設定すべきか
instrumentationを入れると、次の問いがすぐ続く。どこに通知を設定するか。
Google SRE初期にRob Ewaschukが書いた通知哲学の文書は、人を呼び出す通知は緊急で、重要で、対処可能で、実在するものでなければならないと断言する。そして原因ではなく症状に通知するよう勧める。500レスポンスやユーザーに見えるエラーのような外部信号を使うということだ。
しかし、この原則と私の経験には微妙な緊張がある。**私の症状は500ではなかった。**空の統計を伴う200だった。症状ベースの通知は、失敗がstatus codeに現れるという前提に立つが、前述のdifferential observabilityはまさにその前提を壊す状況である。
だから、この原則を反駁すべきだとは思わない。むしろ、「何を症状と定義するか」こそが本当に難しい部分だという結論に至った。このブログの症状はstatus codeではなく、「統計取得関数が既定値を返したこと」であり、手動のinstrumentationを入れて初めて症状になりえた。
同じ文書の別の助言も覚えておく価値がある。騒がしい通知は削除する方向へ傾け、過剰monitoringの方が過少monitoringより解決しにくいというものだ。参考までにSRE本は、postmortemを書くtriggerの一覧へmonitoringの失敗そのものを含める。source mapのアップロードが静かに失敗しないようログを条件付きで残した判断も、同じ方向を指していた。
AI時代に、なぜこの作業がさらに重要になるのか
ここまで読むと、自然な疑問が浮かぶ。これは単にobservabilityのツールを正しく入れようという話ではないか。AIと何の関係があるのか。
私には大いに関係があると思える。理由は二つだ。
第一に、業界データがそう語る。Nathen HarveyとDerek DeBellisが率いた2025 DORA reportは、世界の技術実務者約5,000人の調査と100時間超の定性データに基づく。AI導入がthroughputと製品成果に正の関係を示したと記し、直後に次の文を置く。
しかし、AI導入はソフトウェアdeliveryの安定性と引き続き負の関係を示している。
However, AI adoption does continue to have a negative relationship with software delivery stability.
deliveryの安定性とは依然として負の関係にある。速くなるだけ揺らぐ。DORAは別のinsight記事で仕組みをこう説明する。生成段階で節約した時間が検証のオーバーヘッドへ再配分され、reviewすべきコードが生まれる速度自体も上がる。同じreportの要約が状況を最もよく表す。AIはチームを直さず、すでにあるものを増幅する。observabilityがないままdeploy速度だけを上げれば、増幅されるのは静かな失敗だ。
第二に、こちらの方が私には重かったのだが、**自分の体感を信用できない証拠がある。**METRが2025年に発表した研究は、熟練open-source developer 16人に実際のissue 246件を与え、issueごとにAI利用を許可するか無作為に割り当てた。
developerがAIツールの使用を許可されると、issueの完了に19%長くかかる…
When developers are allowed to use AI tools, they take 19% longer to complete issues…
本当に目を引くのは、その次だ。developerは事前にAIで24%速くなると予想し、**実際に遅くなった経験の後でさえ、20%速くなったと信じていた。**私はAIフロントエンドエンジニアを書いた時にもこの研究を引用し、当時は生産性の文脈で読んだ。今は違う。この数値はAIを使うなという根拠ではなく、体感と現実がずれるという根拠だ。
そして体感を信用できないなら、残る方法は一つしかない。測ることだ。このブログは問題なく動いていると感じていたが、測るとGA呼び出しが1分以上ぶら下がっていた。直したと感じたが、再び測るとまだ終わっていなかった。
観測の対象も広がっている
最後に、最近の流れを一つ記しておきたい。
観測すべき対象自体が変わりつつある。OpenTelemetryは生成AI向けsemantic conventionを別repositoryに分けて整理中で、GenAI clientだけでなくMCP(Model Context Protocol)呼び出しまでinstrumentationの対象に含まれる。まだ初期段階でschemaも整理中のため、現時点で導入を勧める段階ではないと思う。ただし方向は明確だ。AIエージェントにツールを接続して使い始めた以上、その呼び出しも観測対象になる。AIエージェントツールでMCPの動作原理を扱い、Harness(Systems) Engineeringでevalについて書いたが、両者が交わる場所はここなのだと思う。
error monitoringサービスも同じ方向へ動く。SentryはSeerというAI debuggingエージェントを提供し、issue詳細とtrace、ログ、profile contextを組み合わせ、根本原因を見つけて修正PRまで作ると説明している。私はまだ本格的に使っておらず、精度の公式数値も文書では見つからなかった。そのため性能を断定できない。ただし、先ほどMCPでissueを開いた経験と同じ方向なのは明らかだ。観測データを解釈するコストが下がっている。
おわりに
まとめると、こうなる。
AIとともに作業することで、observabilityのツールを導入するハードルは実際に下がった。長く先延ばしにしたことが1日で終わり、ブラウザが指標を作る仕組みのような知識の負担も以前ほど大きくない。ただし、得たものはツールではなかった。routeのcatchだけを捕捉すればよいという仮定、名前どおりオプションが働くという仮定、timeoutを入れたから終わったという仮定が、すべて間違っていたという事実だった。どれも測るまでは分からないことだった。
そこで、私はobservabilityをこう理解するようになった。障害発生時に原因を探すツールであると同時に、それ以前に、システムに対する自分の認識と現実の隔たりを測るツールである。Gray Failure論文の表現を借りれば、異なる主体が何を失敗とみなすか、その隔たりを埋める作業だ。その隔たりは安定性にも、ユーザーが実際に経験していることにも存在する。
生成が安くなるほど、この隔たりは広がりやすい。作る速度は上がるが、確認する速度が同じだけ上がるとは限らない。だから私は最近、AIをもっと積極的に使うために必要なのがobservabilityだと考えるようになった。順序が逆に見えるが、速く作れるなら、速く確認できなければならない。
もちろん、ここに書いたものは個人ブログという小規模な場所で得た観察だ。トラフィックやチーム規模が違えば判断も変わる。78.8KBを諦める決定も、別のサービスなら逆になりうる。正解が一つある領域ではなさそうだ。ただ一つは言えると思う。うまく動いていると感じることと、実際にうまく動いていることは違い、その差を知る方法は測ることしかない。読者にも、自分のサービスについて測らずに信じていること、そして直したと信じたまま開き直していないものを、一度考えてみてほしい。