プログラミング
Show HN: Dllog – 操作失敗時にデバッグログをリプレイする
Show HN: Dllog – Replay debug logs when an operation fails (github.com)
要約
Dllogは、Goサービスにおけるデバッグログの取り扱いに関するトレードオフを解消するライブラリです。成功した操作では情報レベルのログのみを出力し、失敗した操作ではその原因となったデバッグレベルのログを元のタイムスタンプと共にリプレイします。既存のログライブラリ(log/slogやzap)と連携し、操作ごとのバッファリングにより、デバッグ情報のコストを成功時にはかけずに、失敗時のみに提供します。
全文翻訳
Dllogは、すべてのGoサービスが直面する同じ選択肢を解決します:デバッグログは本番環境に溢れかえり、Infoレベルのログはエラーを説明するコンテキストを隠します。Dllogは、操作ごとにこのトレードオフを解消します。操作ごとにバウンドされたリングバッファに低レベルのレコードをバッファリングするため、成功した操作はInfoレベルのロガーと同じくらい静かに保たれ、失敗した操作はそれに至ったデバッグレコードを元のタイムスタンプと共にリプレイします。
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
左の列と同じくらい静かです。
左の列と同じくらい静かです。
左の列と同じくらい静かです。
あなたは真ん中のものが失敗したことを知っています。カードのトークン、リトライ、拒否コードはすべてレベル以下だったので、なぜ失敗したのかはわかりません。
成功した2つのチェックアウトでは、バッファリングされたデバッグレコードが元のタイムスタンプと共にリプレイされ、replay=trueとマークされます。完全なストーリーですが、成功したチェックアウトでもコストがかかるため、誰もこれをオンにしたままにしません。
インストール
go get github.com/arhuman/dllog
Go 1.24以降が必要です。ルートパッケージには依存関係はありません。zapはオプションであり、zapadapterによってのみインポートされます。
使用法
インストルメント化するコードに対応するエントリーポイントを選択してください:
HTTPサーバー:ハンドラーをMiddleware()でラップします。
それ以外:Scopeでスコープを開き、失敗時にTripを呼び出します。
slogの代わりにzapを使用する場合:zapアダプターを参照してください。
HTTP
NewJSONはハンドラーとその出力を生成します。配線する必要のあるものは他にありません:サービスはInfoレベルでログを記録し、失敗したリクエストはそれに至ったデバッグレコードも取得します。
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: invisible on a successful request.
slog.DebugContext(ctx, "loading cart", "user", 42)
slog.DebugContext(ctx, "applying discount", "code", "SUMMER")
// Any Error record replays everything buffered above it first.
slog.ErrorContext(ctx, "payment declined", "provider", "stripe")
w.WriteHeader(http.StatusInternalServerError)
})
// The middleware opens a scope per request, trips on 5xx and on panic.
http.ListenAndServe(":8080", dllog.Middleware()(mux))
}
失敗したリクエストは、それを解放したエラーの前に、マークされ、ログが記録された時刻を運ぶ2つのバッファリングされたデバッグレコードを出力します:
{"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) // replay the buffer, then return the error as usual
return err
}
return nil // buffer discarded, nothing emitted
}
スコープはネストではなく結合します。既にスコープを持つコンテキストで呼び出すと、同じスコープと何も行わないdoneが返されるため、作成者のみがバッファを解放します。
コスト
スコープ外では、Debug呼び出しはプレーンなslogロガー(Infoレベルで設定)の4.2 ns/opに対して9.1 ns/opとゼロアロケーションのコストがかかります。レコードは構築される前に拒否され、差は1つのコンテキストルックアップです。スコープ内では、レコードのバッファリングは約235 nsと1つのアロケーションのコストがかかります。これは、操作が後で失敗した場合に利用できるようにするための価格です。
バッファは厳密にカウントバウンドです:スコープごとに固定の事前割り当てリング(デフォルトで256レコード)があり、最古のものから削除され、ドロップされたものを報告する合成レコードが付随します。バッファリングされた各レコードが保持するものはあなた次第です。スコープが終了するまでレコードは参照を保持するためです(注意点参照)。稼働時間と共に成長することはありません。完全なテーブルと方法論:docs/performance.md。
ドキュメント
ドキュメントの内容
設定
すべてのオプション、出力エンコーディングの選択、既存のハンドラーのラップ
パフォーマンス
ベンチマーク、メモリモデル、ソーク結果
注意点
変更された値、バッファ保持、WithGroup
zapアダプター
zapから同じエンジンを駆動し、そのバインディングコスト
ADR
アーキテクチャ決定レコード
バッファリングされたレコードは現在書き込まれ、後でフォーマットされます。これは、それに依存する前に知っておくべき結果をもたらします。注意点を読んでください。
ステータス
log/slogハンドラー、HTTPミドルウェア、およびzapアダプターは実装およびテスト済みです。どちらのアダプターも他方に依存しておらず、両方とも内部/コアを直接駆動し、どちらかのスコープも他方から可視です。v1前:v0.1.xとしてリリースされており、APIはv1前に変更される可能性があります。リリースはタグ付けされ、CHANGELOGにリストされています。
セキュリティ
公開された問題ではなく、電子メールで脆弱性を報告してください。SECURITY.mdを参照してください。
ライセンス
MIT、LICENSEを参照してください。