朝、生成ログを開くと、記事が一本も出ていませんでした。
原因を追うと、あるコネクタ呼び出しが二晩続けて失敗していたのです。エラーは静かに握り潰され、私の手元には何の信号も届いていませんでした。復旧そのものは五分で済みましたが、二日ぶんの空振りを取り返せないことのほうが、胸に残りました。
その週、Claude のコネクタ可観測性が public beta になりました。管理者はコネクタの採用状況・エラー・レイテンシ・使用量を製品横断で見られる、という案内です。読みながら気づいたのは、私のような個人運用には、その計器盤が手元に無いという当たり前の事実でした。
Enterprise の可観測性を、個人運用にどう翻訳するか
コネクタ可観測性は Team / Enterprise の管理者向け機能です。私が一人で回している夜間ジョブには、そのダッシュボードは付いてきません。
それでも、見たい数字は同じでした。どのコネクタが、どれくらいの頻度で、どれくらいの速さで、どれだけ失敗しているか。この四つが見えていれば、二晩の空振りは初日の朝に捕まえられたはずです。
個人開発で複数のアプリと四つのサイトの処理を夜間に回している身としては、朝に AdMob の数字を確認する習慣はあっても、その数字を運ぶコネクタ自体の健康は一度も見ていませんでした。運ぶ器を疑わずに、運ばれた中身だけを眺めていたわけです。
そこで方針を決めました。製品側の管理機能を待つのではなく、自分のジョブの内側に薄い計器を一枚だけ差し込む。増やすのは記録だけで、処理そのものには触れない。
判断・自動化の外に、観測の層をそっと重ねる。禅でいう「ただ見る」に近い姿勢で、まずは黙って数字を溜めることから始めました。
コネクタ呼び出しを包む、薄いラッパー
やったことは単純です。MCPコネクタの呼び出しを一枚の関数で包み、開始時刻・所要時間・成否・エラーの種別だけを一行の JSONL に落とします。
// connector-meter.ts — コネクタ呼び出しに計器を一枚だけ足す
import { appendFile } from "node:fs/promises";
type Outcome = {
ts: string; // ISO8601(記録時刻)
tool: string; // コネクタ/ツール名
latencyMs: number; // 所要時間
ok: boolean; // 成否
errorClass?: string; // 失敗時の分類
};
const LOG = process.env.METER_LOG ?? "./_meter/connector.jsonl";
// エラーを「一時的か・構造的か」で粗く分類する
function classify(err: unknown): string {
const m = (err instanceof Error ? err.message : String(err)).toLowerCase();
if (m.includes("timeout") || m.includes("etimedout")) return "timeout";
if (m.includes("429") || m.includes("rate")) return "rate_limit";
if (m.includes("401") || m.includes("403") || m.includes("auth")) return "auth";
if (m.includes("5")) return "upstream_5xx";
return "other";
}
export async function meter<T>(
tool: string,
call: () => Promise<T>,
): Promise<T> {
const start = Date.now();
try {
const result = await call();
await record({ tool, latencyMs: Date.now() - start, ok: true });
return result;
} catch (err) {
await record({
tool,
latencyMs: Date.now() - start,
ok: false,
errorClass: classify(err),
});
throw err; // 記録するだけで、握り潰さない
}
}
async function record(o: Omit<Outcome, "ts">): Promise<void> {
const line = JSON.stringify({ ts: new Date().toISOString(), ...o });
await appendFile(LOG, line + "\n", "utf8");
}呼び出し側は、既存のコネクタ呼び出しを meter() でくるむだけです。
const issues = await meter("github.list_issues", () =>
github.listIssues({ repo, state: "open" }),
);ここで大事にしたのは、throw err を残したことです。観測のために例外を飲み込んでしまえば、計器を付けたことがかえって障害を隠します。記録は取る、けれど流れは止めない。この一線だけは守りました。
エラーを黙って握り潰さない姿勢は、以前 失敗を大きな声で叫ばせる入力鮮度の契約 でも書いた考え方の延長にあります。観測は、沈黙を減らすための手段です。
週次で振り返る、小さな集計
溜めた JSONL は、週に一度だけ集計します。コネクタごとに、呼び出し回数・失敗率・レイテンシの中央値と p95 を並べるだけの短いスクリプトです。
# 直近7日を、コネクタ別に集計する
jq -s '
group_by(.tool)[] |
{
tool: .[0].tool,
calls: length,
fail_rate: ((map(select(.ok == false)) | length) / length * 100 | floor),
p50: (map(.latencyMs) | sort | .[(length * 0.50 | floor)]),
p95: (map(.latencyMs) | sort | .[(length * 0.95 | floor)])
}
' _meter/connector.jsonl出てきた表を眺めると、それまで感覚でしか掴めていなかったものが輪郭を持ちました。
| コネクタ | 週の呼び出し | 失敗率 | p50 | p95 |
|---|---|---|---|---|
| github.list_issues | 420 回 | 0% | 310ms | 720ms |
| web.fetch | 1,180 回 | 4% | 640ms | 2,300ms |
| storage.read | 860 回 | 0% | 90ms | 210ms |
上の値は私の環境で観測した一週間ぶんの目安です。平均だけを見ていたら気づけなかった p95 の伸びが、web.fetch に出ていました。ふだんは速いのに、たまに二秒を超える。その「たまに」が、夜間の連続実行では取りこぼしの温床になります。
失敗率の 4% も、内訳を見ると大半が timeout でした。認証切れのような構造的な失敗はゼロ。つまり再試行で救える種類だと分かり、対処の優先順位がはっきりしました。
数字が変えた、しきい値の決め方
計器を付ける前、私は不調を「なんとなく遅い気がする」でしか語れませんでした。いまは、しきい値を数字で決められます。
私の運用では、コネクタ単位で p95 が 1,500ms を超え続けたら黄信号、失敗率が週 5% を超えたら赤信号、と粗く決めています。厳密な最適値ではありません。けれど、線を一本引いたことで、翌朝に見るべき箇所が一目で分かるようになりました。
エラー種別を分けたことも効きました。timeout と rate_limit は待てば直ることが多く、auth はその場で人が動かないと直りません。同じ「失敗」でも、扱いはまるで違います。分類の一列を足しただけで、朝の判断がずいぶん軽くなりました。
この計器は、無音のスキップを検知する仕組みと組み合わせると一段強くなります。ジョブが動いたかどうかは スケジュールジョブのデッドマンスイッチ で、動いた中身の質は今回の計器で。二つを並べて、ようやく夜を任せられる気がしてきました。
これから足したいこと
いま手を付けているのは、週次集計を毎朝の軽い確認に変えることです。前日ぶんだけを集計し、しきい値を越えたコネクタがあれば一行だけ通知する。大げさな監視基盤は要りません。
本格的な計装に踏み込むなら、OpenTelemetry のような標準に寄せる道もあります。そのあたりは OpenTelemetry で AI 呼び出しを可観測にする に整理しました。ただ個人運用では、まず JSONL 一枚から始めるくらいがちょうどよいと感じています。
Enterprise の可観測性は、私の手元には来ません。それでも、見たい四つの数字を自分で溜めるだけで、夜の自動実行に対する安心はずいぶん変わりました。計器は、派手な機能ではありません。けれど、静かに落ちていたコネクタにもう一度気づけるようにする、確かな一手でした。
実装の参考になれば幸いです。お読みいただき、ありがとうございました。