
2025/12/13 0:46
I couldn't find a logging library that worked for my library, so I made one
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
要約
Fedify(ActivityPub サーバーフレームワーク)は、適切なライブラリレベルのロギングソリューションを欠いていました。Winston、Pino、debug など既存の JavaScript ロガーはアプリケーション中心で、明示的な設定が必要だったり非構造化テキストを出力したりするため、ゼロオーバーヘッドデフォルトを必要とするライブラリには不向きでした。
著者は LogTape を作成しました。これはライブラリ専用に設計された軽量ロガーです。LogTape は階層的なカテゴリ(例:
fedify、federation inbox/outbox/http、sig http/ld/key、runtime docloader、webfinger lookup)を定義し、configure API を通じて明示的に有効化されたときだけ構造化 JSON ログを出力します。デフォルトでは Fedify はログを生成せず、パフォーマンスオーバーヘッドを排除します。最小限の設定であれば、エラーレベルのロギングを全体に有効化できます。
LogTape は受信リクエストヘッダー(
X‑Request‑Id、Traceparent)から抽出した requestId を各ログエントリに自動的にタグ付けします。これは AsyncLocalStorage を使用しており、暗黙のコンテキストとして機能します。開発者は jq などで requestId によってログをフィルタリングし、非同期境界間でイベントを相関させることができます。構造化 JSON ログはファイルや他のシンクに書き込むことができ、観測ツールとの統合を容易にします。
LogTape は Node.js、Deno、Bun、およびエッジ関数でネイティブに動作し、ポリフィルを必要とせず、Winston や Pino などのアプリケーションロガーに対するライブラリ中心の代替手段となります。
本文
はじめに
Fedify(ActivityPub サーバーのフレームワーク)を作り始めたとき、思いがけない問題に直面しました。それは「ロギングをどう追加すればいいか分からない」ということです。
ロギング自体が難しいわけではありません。JavaScript には成熟したロギングライブラリが数多く存在します。しかしそれらは主に アプリケーション 用に設計されており、ライブラリ が「目立たないままで済む」ような設計になっていませんでした。
数か月前にこの点について書いたとき、反応は控えめでした。興味を示す声もあれば懐疑的な声もあり、投稿が AI 生成かどうかという議論までも起きました。正直に言うと、英語は母国語ではないため LLM を使って文章を磨いていますが、アイデアや技術内容は私自身のものです。
読者からは理論よりも「実際の例」を見たいという声が多く寄せられました。そこでこの記事を書きます。
問題点
既存のロガーは「アプリケーションを作る前提」で設計されています。
Fedify は ActivityPub プロトコルを使ってフェデレーテッドなソーシャルアプリを構築するためのライブラリです。フェデレーションに慣れた開発者なら、デバッグがいかに面倒になるか知っています。あるアクティビティが配信できないときは、次のような疑問を解決しなければなりません。
- HTTP リクエストは実際に送信されたか?
- 署名は正しく生成されたか?
- 遠隔サーバーが拒否した理由は何か?
- レスポンスの解析で問題が起きたか?
これらは「HTTP ハンドリング」「暗号署名」「JSON‑LD 処理」「キュー管理」など複数のサブシステムにまたがります。十分なロギングが無ければ、デバッグは推測ゲームになってしまいます。
しかしライブラリ作成者として直面したジレンマは次の通りです。
- 詳細ログを追加すると、Fedify の内部情報でコンソールが乱雑になる恐れがあります。
- 何も出力しないと、ユーザーは問題を特定できず苦労します。
既存オプションを調べた結果:
| ライブラリ | 仕組み |
|---|---|
| winston / Pino | ① Fedify 内でロガーを構成(自分の選択肢を押し付ける) ② ユーザーにロガーインスタンスを渡させる(余計なコードが必要) |
| debug | 用途は合致するが、構造化されたレベルベースのログを提供せず、環境変数依存で Deno など一部ランタイムでは制限されやすい |
どれも「ライブラリ向けには不十分」でした。そこで私は LogTape をゼロから設計し、Fedify がその最初の実ユーザーとなりました。
解決策:階層化カテゴリとデフォルトで出力なし
鍵は「ライブラリがログを出力できるが、アプリ側が明示的に許可するまで何も表示しない」ことです。Fedify は LogTape の階層化カテゴリシステムを使い、ユーザーが細かく制御できます。
カテゴリ構成例
| カテゴリ | ログ対象 |
|---|---|
| ライブラリ全体 |
| 受信アクティビティ |
| 発信アクティビティ |
| HTTP リクエスト/レスポンス |
| HTTP Signature 処理 |
| Linked Data Signature 処理 |
| キー生成・取得 |
| JSON‑LD ドキュメント読み込み |
| WebFinger ルックアップ |
ほぼ 10 種類のサブシステムを表しており、ユーザーは必要な部分だけを有効にできます。
await configure({ sinks: { console: getConsoleSink() }, loggers: [ // Fedify 全体のエラーのみ表示 { category: "fedify", sinks: ["console"], lowestLevel: "error" }, // inbox 処理に関してはデバッグ情報を追加 { category: ["fedify", "federation", "inbox"], sinks: ["console"], lowestLevel: "debug", }, ], });
エラーが発生したときだけ詳細ログが出力され、他のサブシステムは静かに保たれます。コードを変更する必要はなく、設定だけで済みます。
隠しコンテキストでリクエスト追跡
階層化カテゴリでフィルタリングは解決できましたが、非同期境界間でログを結びつける課題も残っていました。
フェデレーテッドシステムでは「1 つのユーザー操作」が複数の処理を連鎖させます(遠隔アクター取得 → 署名検証 → アクティビティ処理 → フォロワーへの配信)。失敗した際に、すべてのログエントリを一つの「リクエスト」に紐づける必要があります。
LogTape の implicit context 機能で、すべてのログエントリに自動的に
requestId を付与します。
await configure({ sinks: { file: getFileSink("fedify.jsonl", { formatter: jsonLinesFormatter }), }, loggers: [{ category: "fedify", sinks: ["file"], lowestLevel: "info" }], contextLocalStorage: new AsyncLocalStorage(), // implicit contexts を有効化 });
この設定で、すべてのログに
requestId が付加されます。特定リクエストをデバッグしたいときは次のように絞り込み可能です。
jq 'select(.properties.requestId == "abc-123")' fedify.jsonl
これで「同じリクエスト」に属するすべてのログが順序通りに表示され、手動で結びつける必要はありません。
requestId は X‑Request‑Id や Traceparent など既存ヘッダーから自動取得できるため、観測基盤との統合もスムーズです。
実際にユーザーが見るもの
Fedify を使う人は次のような体験をします。
| 状況 | 何が起こるか |
|---|---|
| LogTape 未設定 | 出力なし。警告やデフォルト出力も無く、性能への影響はほぼゼロ。 |
| エラーレベルのみ有効化 | Fedify 全体のエラーだけを 3 行の設定で確認可能。 |
| 特定サブシステムのデバッグ | 必要なカテゴリだけ レベルに切り替えることで詳細ログが取得できる。 |
| 本番環境で監視要求がある場合 | 構造化 JSON ログを監視システムへ送信し、リクエスト ID による相関情報付きで収集可能。 |
Node.js・Deno・Bun・Edge Functions など、どのランタイムでも同じライブラリコードが動作します。ポリフィルやシムは不要です。
学んだこと
-
カテゴリ設計を早めに決定する
階層化構造は「ユーザーが実際にログを絞り込みたい観点」を反映させるべきです。Fedify ではサブシステムごとに分けました。 -
構造化ロギングを採用する
、requestId
、activityId
などのプロパティは文字列補間よりも後で解析しやすく、運用上非常に有用です。actorId -
implicit context が不可欠
非同期境界を跨いだログの相関付けを手動で行う必要がなくなるため、分散処理のデバッグが格段に楽になります。ユーザーは 1 行の jq コマンドで全情報を取得できます。 -
ユーザーを信頼する
ライブラリ作者として「内部詳細を漏らすこと」を恐れるよりも、必要なときにだけ表示できる設計が重要です。オプトイン型にすることで、ユーザーは自由に選択できます。
ぜひ試してみてください
ライブラリ開発で「ロギングの量」「ユーザー制御」「ノイズを抑える方法」について悩んでいるなら、Fedify の実装を参考にすると良いでしょう。
Fedify のロギングドキュメントには詳細が記載されていますし、LogTape の設計哲学を知りたい方は私の以前の記事をご覧ください。
LogTape は「アプリケーション向けの winston や Pino を置き換える」ことを目的としたものではありません。
「ライブラリがユーザーに干渉せず、必要な時だけ活躍できるロギング」を提供するためのユニークな位置づけです。もしそのような要件をお持ちなら、従来のアプリ中心ロガーよりも LogTape が適しているかもしれません。