HTTPトレーシングの紹介

Introducing HTTP Tracing by Jaana Burcu Dogan

はじめに

Go 1.7で、HTTPクライアントリクエストのライフサイクル全体を通してきめ細かい情報を収集する機能である HTTPトレーシングを導入しました。HTTPトレーシングのサポートは net/http/httptrace パッケージによって提供されています。 収集した情報は、レイテンシの問題のデバッグ、サービスの監視、適応型システムの構築など、 さまざまな用途に利用できます。

HTTPイベント

httptrace パッケージは、HTTPラウンドトリップの間にさまざまなイベントに関する情報を収集するための、 多数のフックを提供します。これらのイベントには次のものが含まれます。

  • コネクションの作成
  • コネクションの再利用
  • DNSルックアップ
  • リクエストのワイヤーへの書き込み
  • レスポンスの読み込み

イベントのトレーシング

フック関数を含む *httptrace.ClientTrace を リクエストの context.Context に格納することで、HTTPトレーシングを 有効にできます。さまざまな http.RoundTripper の実装は、 コンテキストの中にある *httptrace.ClientTrace を探し、関連するフック関数を呼び出すことで、 内部のイベントを報告します。

トレーシングはリクエストのコンテキストにスコープされるので、利用者はリクエストを開始する前に *httptrace.ClientTrace をリクエストのコンテキストに格納しておく必要があります。

req, _ := http.NewRequest("GET", "http://example.com", nil)
trace := &httptrace.ClientTrace{
    DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
        fmt.Printf("DNS Info: %+v\n", dnsInfo)
    },
    GotConn: func(connInfo httptrace.GotConnInfo) {
        fmt.Printf("Got Conn: %+v\n", connInfo)
    },
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
if _, err := http.DefaultTransport.RoundTrip(req); err != nil {
    log.Fatal(err)
}

ラウンドトリップの最中、http.DefaultTransport はイベントが発生するたびに各フックを呼び出します。 上記のプログラムは、DNSルックアップが完了するとすぐにDNS情報を出力します。同様に、 リクエスト先のホストへのコネクションが確立されると、コネクションの情報を出力します。

http.Clientでのトレーシング

このトレーシングの仕組みは、単一の http.Transport.RoundTrip のライフサイクルにおけるイベントを トレースするよう設計されています。しかし、クライアントは1つのHTTPリクエストを完了するために 複数回のラウンドトリップを行うことがあります。たとえば、URLリダイレクトが発生する場合、 クライアントがHTTPリダイレクトに追従するたびに複数のリクエストが発行され、登録済みのフックは その回数分だけ呼び出されます。利用者は、このようなイベントを http.Client のレベルで認識する 責任を負います。以下のプログラムは、http.RoundTripper のラッパーを使って現在のリクエストを 識別しています。

package main

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httptrace"
)

// transport は処理中のリクエストを追跡し、HTTPトレーシングイベントを報告する
// フックを実装する http.RoundTripper です。
type transport struct {
    current *http.Request
}

// RoundTrip は現在のリクエストを追跡するために http.DefaultTransport.RoundTrip を
// ラップします。
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
    t.current = req
    return http.DefaultTransport.RoundTrip(req)
}

// GotConn は、現在のリクエストに対してコネクションが以前に使用されたことが
// あるかどうかを出力します。
func (t *transport) GotConn(info httptrace.GotConnInfo) {
    fmt.Printf("Connection reused for %v? %v\n", t.current.URL, info.Reused)
}

func main() {
    t := &transport{}

    req, _ := http.NewRequest("GET", "https://google.com", nil)
    trace := &httptrace.ClientTrace{
        GotConn: t.GotConn,
    }
    req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

    client := &http.Client{Transport: t}
    if _, err := client.Do(req); err != nil {
        log.Fatal(err)
    }
}

このプログラムはgoogle.comからwww.google.comへのリダイレクトに追従し、 次のように出力します。

Connection reused for https://google.com? false
Connection reused for https://www.google.com/? false

net/http パッケージの Transport は、HTTP/1とHTTP/2の両方のリクエストのトレーシングを サポートしています。

独自の http.RoundTripper の実装を作成する場合は、リクエストのコンテキストに *httptest.ClientTrace があるかを確認し、イベントが発生した際に関連するフックを呼び出すことで、 トレーシングをサポートできます。

まとめ

HTTPトレーシングは、HTTPリクエストのレイテンシのデバッグや、送信トラフィックのネットワーク デバッグ用ツールの作成に関心がある人にとって、Goへの価値ある追加機能です。この新機能を導入する ことで、httpstat のような、HTTPのデバッグ、ベンチマーク、 可視化のためのツールがコミュニティから生まれることを期待しています。

By Jaana Burcu Dogan