
2026/05/29 1:46
Go の net/http/httptrace を用いた HTTP リクエストの追跡
RSS: https://news.ycombinator.com/rss
要約▶
日本語翻訳:
Go の標準ライブラリの
net/http パッケージは、外部プロキシやサードパーティ製エージェントを必要とせずに、HTTP パフォーマンスのボトルネックを診断するための強力な内蔵ソリューションを提供します。httptrace パッケージ(Go 1.7 から利用可能)を活用することで、開発者は外出先リクエストの各ステージを観察するために特定のフックを付け加えることができ、DNS リゾリューションや接続取得から TLS ハンドシェイク、ボディ消費までを含みます。この機能は、httptrace.WithClientTrace を用いて ClientTrace 構造体をリクエストコンテキストにリンクするか、またはトランスポート層を TracingTransport でラップすることで実現されます。ClientTrace はオプションの関数フィールドを持ち、未設定のフィールドは nil としてスキップされるため、新しいフックが追加された際にも後方互換性を保ち、複数の観測セットが無縫に連携することを可能にします。ボディ消費を含む正確な総実行時間の測定のためには、レスポンスボディを特殊な timedBody でラップします。これらの機能はコンテキストの伝播を介して既存のミドルウェアと自然に統合されるため、エンジニアはすぐに詳細なタイミング分解を示す curl ユーティリティに似た、自己完結型の Go プログラムを実装できます。このアプローチは外部依存関係の排除と、接続の再利用(GotConnInfo.Reused および WasIdle 経由)に対する粒度の細かな可視性提供によってデバッグを簡素化し、APM エージェントやインストルメンテーションライブラリを必要とせずに、DNS 解決や TLS ハンドシェイクなど遅いコンポーネントをネイティブコードベース内で迅速に特定することを可能にします。本文
Go の標準ライブラリ net/http/httptrace
で HTTP リクエストの詳細をトレースする
net/http/httptrace2026 年 5 月 26 日公開
Go 1.7 に導入された標準ライブラリ
は、トランスポートの外から HTTP リクエストの内部動作を詳細に観測できます。しかし、多くの Go 開発者がまだ利用していないのが現状です。net/http/httptrace
httptrace
が公開するフックポイント
httptraceこのパッケージは、以下のようなネットワーク上の重要な瞬間におけるフックを提供します。
- DNS の名前解決 (
,DNSStart
)DNSDone - コネクションの取得 (
,ConnectStart
)ConnectDone - TLS ハンドシェイク (
,TLSHandshakeStart
)TLSHandshakeDone - ネットワーク上の転送開始 (
)GetConn - 最初の応答バイト受信 (
)GotFirstResponseByte
これらは、従来のログやメトリクスだけでは捉えることが困難な「搬送層の外」とからの観測を可能にします。
設計思想:なぜ context
を使っているのか?
contextインターフェースではなく *ClientTrace
コンテキストを採用した理由
*ClientTrace直感的な設計(
Tracer インターフェースをフィールドとして持つ)とは異なる、Go の標準ライブラリのアプローチには明確な利点があります。
- ミドルウェアとの互換性: 追跡情報は
に格納されるため、コンテキストを中継するあらゆるミドルウェアで自動的に動作します。追加のコストなしで実装が広がります。context.Context - 無状態設計: 共有可変状態が存在しないため、同じ
で複数のリクエストを送信しても、それぞれ異なる追跡情報を安全に管理できます。http.Client - ゼロコストのオプション: リクエストに追跡を付与しないとトランスポートは無視するため、「未使用時のオーバーヘッド」はほぼありません。
追跡対象とするのは任意のフィールド(関数)のみで構いません。設定されていないフィールドは
nil としてスキップされます。
// クライアント作成サンプル trace := &httptrace.ClientTrace{ DNSStart: func(info httptrace.DNSStartInfo) { fmt.Printf("DNS start: %s\n", info.Host) }, GotFirstResponseByte: func() { fmt.Println("First byte received") }, } ctx := httptrace.WithClientTrace(context.Background(), trace) req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
実装例:curl --trace
に似た CLI ツール
curl --traceURL を受け取り、各フックのタイミングを計測して出力するシンプルな CLI ツールを作成できます。これにより、DNS ルックアップや TLS ハンドシェイクなど、どこに時間がかかるかが一目でわかります。
タイミング計測構造体
type timings struct { start time.Time dnsStart time.Time dnsDone time.Time connectStart time.Time connectDone time.Time tlsStart time.Time tlsDone time.Time gotConn time.Time firstByte time.Time done time.Time } func (t *timings) elapsed(at time.Time) time.Duration { return at.Sub(t.start) }
クライアントトレース作成
func newTrace(t *timings) *httptrace.ClientTrace { return &httptrace.ClientTrace{ DNSStart: func(_ httptrace.DNSStartInfo) { t.dnsStart = time.Now() }, DNSDone: func(_ httptrace.DNSDoneInfo) { t.dnsDone = time.Now() }, ConnectStart: func(_, _ string) { t.connectStart = time.Now() }, ConnectDone: func(_, _ string, _ error) { t.connectDone = time.Now() }, TLSHandshakeStart: func() { t.tlsStart = time.Now() }, TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { t.tlsDone = time.Now() }, GotConn: func(_ httptrace.GotConnInfo) { t.gotConn = time.Now() }, GotFirstResponseByte: func() { t.firstByte = time.Now() }, } }
メイン処理と出力
func main() { url := os.Args[1] t := &timings{start: time.Now()} trace := newTrace(t) ctx := httptrace.WithClientTrace(context.Background(), trace) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } res, err := http.DefaultClient.Do(req) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } res.Body.Close() t.done = time.Now() // 分解表示出力 fmt.Printf("DNS lookup: %v\n", t.dnsDone.Sub(t.dnsStart)) fmt.Printf("TCP connect: %v\n", t.connectDone.Sub(t.connectStart)) fmt.Printf("TLS handshake: %v\n", t.tlsDone.Sub(t.tlsStart)) fmt.Printf("Server processing: %v\n", t.firstByte.Sub(t.gotConn)) fmt.Printf("Content transfer: %v\n", t.done.Sub(t.firstByte)) fmt.Printf("Total: %v\n", t.done.Sub(t.start)) }
特徴: 依存グラフ内の計測ライブラリも不要で、プロキシや APM エージェントなしでも動作します。
注意点:
/DNSStart: ドメインが既にキャッシュされている場合はフックが発火しないことがあります。DNSDone : HTTPS の場合のみ発火します。TLSHandshake : コネクションの再利用の有無はGotConnフラグで確認可能。info.Reused
実装例:すべてのリクエストを追跡する RoundTripper
RoundTripper特定のクライアントを通じて自動追跡を行いたい場合は、
http.RoundTripper をラップしてコンテキストに追跡情報を付与します。
トレース付きトランスポートの定義
type TracingTransport struct { Base http.RoundTripper Log func(req *http.Request, t *timings) } func (tt *TracingTransport) RoundTrip(req *http.Request) (*http.Response, error) { base := tt.Base if base == nil { base = http.DefaultTransport } t := &timings{start: time.Now()} trace := newTrace(t) // コンテキストに追跡情報を追加(既存があれば上書きされます) req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) res, err := base.RoundTrip(req) t.done = time.Now() if tt.Log != nil { tt.Log(req, t) } return res, err }
使用例
client := &http.Client{ Transport: &TracingTransport{ Log: func(req *http.Request, t *timings) { log.Printf("%s %s dns=%v tls=%v ttfb=%v total=%v", req.Method, req.URL, t.dnsDone.Sub(t.dnsStart), t.tlsDone.Sub(t.tlsStart), t.firstByte.Sub(t.gotConn), t.done.Sub(t.start), ) }, }, } client.Get("https://example.com")
コンポジションの注意点: 呼び出し元がすでに
を設定した場合、単純な置換ではなく統合されます。最後の追跡情報が優先されますが、両方のフックが発火する可能性があります。ClientTrace
ボディ転送時間の正確な計測
重要なのは、
t.done が**「レスポンスボディを読み終えた時点」で測定されること**です。
現在の RoundTrip ではヘッダー読み取りまでしか計測されません。全体の転送時間を正しく測るには、ボディリーダーをラップしてクローズ時に時刻を取得する必要があります。
type timedBody struct { io.ReadCloser onClose func() } func (tb *timedBody) Close() error { tb.onClose() return tb.ReadCloser.Close() } // RoundTrip 内での使用例: res.Body = &timedBody{ ReadCloser: res.Body, onClose: func() { t.done = time.Now() }, }
コネクションの再利用(プール)の確認
httptrace は、クライアントがコネクションを適切に再利用しているかを確認する強力な手段を提供します。
GotConnInfo 構造体には以下のフィールドが含まれます:
: コネクションが再利用されたかどうかReused bool
: 無負荷状態にあったかWasIdle bool
: 無負荷時間の長さ(再利用時のみ)IdleTime time.Duration
GotConn: func(info httptrace.GotConnInfo) { t.gotConn = time.Now() if info.Reused { log.Printf("connection reused (idle for %v)", info.IdleTime) } else { log.Printf("new connection to %s", info.Conn.RemoteAddr()) }, },
トラブルシューティング:
- 同一ホストへの反復呼び出しで常に
が出る場合、コードが意図せずコネクションプールを妨げている可能性があります(例:リクエストごとのクライアント作成、未クローズのボディなど)。Reused: false
まとめ
net/http/httptrace は以下の理由から非常に有用です。
- 低いオーバーヘッド: 無効化可能なオプション設計であり、利用しない場合のコストはほぼゼロです。
- コンテキストベースの設計:
を介して動作するため、標準ライブラリの他のコンポーネントやミドルウェアとシームレスに連携します。context.Context - 手軽なデバッグツール: 外部エージェントを使わず、単一ファイルで HTTP 呼び出しのボトルネックを特定できます。
詳細な追跡情報が必要な場合、Limeleaf のような Web サイトや API 監視ツールを検討することをお勧めします。