Show HN: Dllog – 操作失敗時にデバッグログをリプレイする
Show HN: Dllog – Replay debug logs when an operation fails (github.com)
要約
Dllogは、Goサービスにおけるデバッグログの取り扱いに関するトレードオフを解消するライブラリです。成功した操作では情報レベルのログのみを出力し、失敗した操作ではその原因となったデバッグレベルのログを元のタイムスタンプと共にリプレイします。既存のログライブラリ(log/slogやzap)と連携し、操作ごとのバッファリングにより、デバッグ情報のコストを成功時にはかけずに、失敗時のみに提供します。
全文翻訳
dllog
[画像: CI] [画像: Go Reference] [画像: Go Report Card] [画像: License: MIT]
すべてのGoサービスは同じ選択に直面します: Debugはプロダクションを氾濫させ、Infoはエラーを説明するコンテキストを隠します。dllogはオペレーションごとにこのトレードオフを解消します。バッファリングされた低レベルのレコードを、オペレーションごとに制限されたリングバッファに保持するため、成功したオペレーションはInfoレベルのロガーと同じくらい静かであり続ける一方、失敗したオペレーションは、元のタイムスタンプと共に、それに至ったDebugレコードをリプレイします。
dllogはロギングライブラリではなく、既存のライブラリを置き換えるものではありません。現在、log/slogおよびzapと連携して動作し、同じサービス内で混在させることも可能です。他のライブラリも同様の方法でサポートできます。
同じ失敗、3つの方法
3つのチェックアウトを、同じプログラムで3回ログ記録しました。1つは成功、1つは支払い拒否で終了、もう1つは再び成功しました。go run ./examples/demo [info|debug|dllog]で、いずれかの列を再現できます。
slog at Info | dllog at Info | slog at Debug |
|---|---|---|
| [画像: slog at Info: the error with no context] | [画像: dllog at Info: the error, with the Debug records that led to it replayed] | [画像: slog at Debug: every record, on every request] |
| 左の列で失敗したことがわかります。しかし、カードトークン、リトライ、拒否コードはすべてレベル以下であったため、その理由はわかりません。 | 成功した2つのオペレーションでは左の列と同じくらい静かです。失敗したオペレーションでは、バッファリングされたDebugレコードが元のタイムスタンプと共にreplay=trueとしてマークされてリプレイされます。 | 全てのストーリーが記録されますが、成功したチェックアウトでもそのコストを支払うことになります。だから誰もこれを有効にしたままにはしません。 |
インストール
go get github.com/arhuman/dllog
Go 1.24以降が必要です。ルートパッケージには依存関係はありません。zapはオプションであり、zapadapterによってのみインポートされます。
使用方法
インストゥルメントするコードに合わせて、エントリポイントを選択してください:
- HTTPサーバー: ハンドラを
Middleware()でラップします。 - その他すべて:
Scopeでスコープを開き、失敗時にTripを呼び出します。 - slogの代わりにzapを使用している場合: zapアダプターを参照してください。
HTTP
NewJSONはハンドラとその出力を生成します。他に設定する必要はありません。サービスはInfoレベルでログを記録し、失敗したリクエストはそれに至ったDebugレコードも取得します。
package main
import (
"log/slog"
"net/http"
"os"
"github.com/arhuman/dllog"
)
func main() {
logger := slog.New(dllog.NewJSON(os.Stderr))
slog.SetDefault(logger)
mux := http.NewServeMux()
mux.HandleFunc("/order", func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Buffered: successful requestでは非表示。
slog.DebugContext(ctx, "loading cart", "user", 42)
slog.DebugContext(ctx, "applying discount", "code", "SUMMER")
// Errorレベルのレコードは、それより前のバッファリングされたすべてをリプレイします。
slog.ErrorContext(ctx, "payment declined", "provider", "stripe")
w.WriteHeader(http.StatusInternalServerError)
})
// ミドルウェアはリクエストごとにスコープを開き、5xxエラーやパニック時にトリップします。
http.ListenAndServe(":8080", dllog.Middleware()(mux))
}
失敗したリクエストは、2つのバッファリングされたDebugレコードを、それらを解放したエラーの前に、タイムスタンプと共にマークされて出力します:
{"time":"2026-09-12T10:23:06.226402+02:00","level":"DEBUG","msg":"loading cart","user":42,"replay":true}
{"time":"2026-09-12T10:23:06.226433+02:00","level":"DEBUG","msg":"applying discount","code":"SUMMER","replay":true}
{"time":"2026-09-12T10:23:06.226434+02:00","level":"ERROR","msg":"payment declined","provider":"stripe"}
成功したリクエストは何も出力しません。スコープが終了するとバッファは破棄されます。
HTTP以外
スコープは自分で管理します。Tripは、エラーがログ記録されるのではなく返されるというGoの一般的なケースをカバーします:
func process(ctx context.Context, id string) error {
ctx, done := dllog.Scope(ctx)
defer done()
slog.DebugContext(ctx, "fetching record", "id", id)
if err := doWork(ctx); err != nil {
dllog.Trip(ctx) // バッファをリプレイし、通常通りエラーを返します
return err
}
return nil // バッファは破棄され、何も出力されません
}
Scopeはネストするのではなく結合します。既にスコープを持つコンテキストで呼び出すと、同じスコープと何も行わないdoneが返されるため、バッファを解放するのは作成者のみです。
コスト
スコープ外では、Debug呼び出しは9.1 ns/opでゼロアロケーションであり、Infoレベルで設定されたプレーンなslogロガーの4.2 ns/opと比較して、差は1回のコンテキストルックアップです。スコープ内では、レコードのバッファリングは約235 nsと1つのアロケーションがかかります。これは、オペレーションが後で失敗した場合に利用できるようにするためのコストです。
バッファは厳密にカウント制限されています。各スコープに対して固定の事前割り当てリングバッファがあり、デフォルトで256レコードです。最も古いものから順に削除され、ドロップされたレコードがあれば報告する合成レコードが生成されます。バッファリングされた各レコードが保持するものはあなた次第です。レコードはスコープが終了するまで、あなたがログに記録したものへの参照を保持します。詳細は注意点を参照してください。稼働時間と共に増加することはありません。
完全なテーブルと方法論: docs/performance.md。
ドキュメント
| ドキュメント | 内容 |
|---|---|
| 設定 | すべてのオプション、出力エンコーディングの選択、既存のハンドラのラップ |
| パフォーマンス | ベンチマーク、メモリモデル、長時間稼働テストの結果 |
| 注意点 | 値の変更、バッファ保持、WithGroup |
| zapアダプター | zapからの同じエンジンの駆動、およびそのバインディングコスト |
| ADRs | アーキテクチャ決定記録 |
バッファリングされたレコードはすぐに書き込まれ、後でフォーマットされるため、それに依存する前に知っておくべき影響があります。詳細は注意点をお読みください。
ステータス
log/slogハンドラ、HTTPミドルウェア、およびzapアダプターは実装され、テストされています。どちらのアダプターも他方を基盤としていません。両方とも直接internal/coreを駆動し、どちらかのScopeも他方から可視です。
v1前: v0.1.xとしてリリースされており、v1の前にAPIが変更される可能性があります。リリースはタグ付けされ、CHANGELOGにリストされています。
セキュリティ
脆弱性は、公開イシューではなく、メールで報告してください。詳細はSECURITY.mdを参照してください。
ライセンス
MIT、詳細はLICENSEを参照してください。