前言
在撰寫 HTTP request test 測試程式時,除了測試 response 結果是否如預期之外,我們還需要知道過程中需要耗費多少時間(request latency)。市面上有一些 libraries (e.g opencensus
) 能提供相關的 HTTP 事件 trace,不過仔細看會發現他們大多也是整合 golang 本身提供的 httptrace
來實現追蹤功能,因此我們就直接來了解 httptrace
的運作原理和應用方式,再結合 http/httptest
,讓 handler test 更完整。
Request 處理流程
在說明 httptrace
之前,由於 request test 中是藉由 http package 來發起的 ,所以先來了解一下 golang 中,透過 http
package 的 client 處理 http request 的流程。
New Request
首先我們透過 http.Get
發出 request, 過程中會經由 NewRequest 產出 request instance 並交由 client.Do(request) method 處理。
1http.Get("/example")
2
3// is equal to:
4
5req := http.NewRequest("GET", "/example", nil)
6resp := http.DefaultClient.Do(req)
Send Request
client.do
method 主要負責發送 request 和回傳 response,可說是整段流程的核心,而過程中會先處理 HTTP Headers 相關內容,接著將 request 正式透過 client.send
送出。
1// source code from client.go
2
3// 1. Client Do
4func (c *Client) do(req *Request) (retres *Response, reterr error) {
5 // ...some code to deal with Headers
6 if resp, didTimeout, err = c.send(req, deadline); err != nil {
7 // error handle
8 }
9 // ...more
10}
11
12// 2. Send Request
13func (c *Client) send(req *Request, deadline time.Time)
14 (resp *Response, didTimeout func() bool, err error) {
15 // ...some code to add cookies
16 // send request with transport method
17 resp, didTimeout, err = send(req, c.transport(), deadline)
18 // ...more
19}
RoundTripper (Transport)
接著來到重頭戲啦,上面 code 中可以看到 c.transport()
,它會回傳 RoundTripper
,真正執行整個 http transaction 就是 RoundTripper 中的 RoundTrip
method。
1// 3. Start RoundTrip
2func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
3 // ...more
4 resp, err = rt.RoundTrip(req)
5 // ..more
6}
RoundTripper
是一個 interface,http package 在預設情況下是使用實作 RoundTripper 的 DefaultTransport 進行基本傳輸, 如果對於 http 流程很清楚的使用者,當然可以自己自製一個 transport 來完成整個 transaction。
緊接著繼續看 DefaultTransport 是如何實作 RoundTrip 的。
1// source code from transport.go
2
3// internel RoundTrip methods
4func (t *Transport) roundTrip(req *Request) (*Response, error) {
5 // get trace from ctx
6 ctx := req.Context()
7 trace := httptrace.ContextClientTrace(ctx)
8}
頭幾行馬上看到我們今天要介紹的主角 - httptrace
中的 trace instance,它在 roundTrip
method 被取出來,而從這邊我們就可以很快地了解到為什麼 httptrace
可以實現追蹤 http 的流程了,因為在接下來的 transaction 過程中,這個 trace 中的 functions 都會在適時的階段觸發。
1func (pc *persistConn) readResponse(rc requestAndChan, trace *httptrace.ClientTrace) (resp *Response, err error) {
2 if trace != nil && trace.GotFirstResponseByte != nil {
3 if peek, err := pc.br.Peek(1); err == nil && len(peek) == 1 {
4 // trigger trace hook functions
5 trace.GotFirstResponseByte()
6 }
7 }
8}
![httptrace]({{ site.url }}/assets/images/httptrace-1.png)
httptrace packages
httptrace 主要提供 HTTP client requests 的事件中追蹤,package 中包含了 ClientTrace
struct,我們可以客製化 ClientTrace
中的 methods,並把 trace 放入 request context,這樣的話,這個 trace 就會在上面剛剛所提到的 roundTrip 中被觸發。
Package httptrace provides mechanisms to trace the events within HTTP client requests.
目前 ClientTrace 有幾個 hooks methods ,分別是:
GetConn func(hostPort string)
- called before a connection is createdGotConn func(GotConnInfo)
- called after a successful connection is obtainedPutIdleConn func(err error)
- called when the connection is returned to the idle poolGotFirstResponseByte func()
Got100Continue func()
Got1xxResponse func(code int, header textproto.MIMEHeader) error
DNSStart func(DNSStartInfo)
DNSDone func(DNSDoneInfo)
ConnectStart func(network, addr string)
ConnectDone func(network, addr string, err error)
TLSHandshakeStart func()
TLSHandshakeDone func(tls.ConnectionState, error)
WroteHeaderField func(key string, value []string)
- called after the Transport has written each request headerWroteHeaders func()
Wait100Continue func()
WroteRequest func(WroteRequestInfo)
- called with the result of writing the request and any body
蠻多節點可以追蹤的,就看使用者自己的需求。
實作
了解上述的流程之後,我們就可以實際實作一次,在 request test 中追蹤 http request 流程了! 假設我們要追蹤 GotConn
到 GotFirstResponseByte
這段時間。
- 首先引入必要的 packages
1import (
2 "time"
3 "net/http"
4 "net/http/httptest"
5 "net/http/httptrace"
6)
- 接著產生一個包含我們自己實作 methods 的 trace
1type Tracer struct {
2 start time.Time
3 end time.Time
4 latency time.Duration
5}
6
7func (l *Tracer) GotConn(connInfo httptrace.GotConnInfo) {
8 l.start = time.Now()
9}
10
11func (l *Tracer) GotFirstResponseByte() {
12 l.end = time.Now()
13 l.latency = l.end.Sub(l.start)
14}
15
16trace := &httptrace.ClientTrace{
17 GotConn: tracer.GotConn,
18 GotFirstResponseByte: tracer.GotFirstResponseByte,
19}
- 然後把 trace 放入 request context 內
1req, _ := http.NewRequest("POST", server.URL, bytes.NewReader(data))
2req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
- 執行 RoundTrip
1// Use DefaultTransport directly
2res, err := http.DefaultTransport.RoundTrip(req)
剛剛上述流程中有介紹到,預設下是使用 http.DefaultTransport
這個 RoundTripper
來執行 HTTP transaction 的,所以我們在測試時,就直接呼叫 http.DefaultTransport.RoundTrip(req)
即可。如此一來,就可以看到剛剛所設定好的 trace hooks 在過程中被觸發,同時也會獲得最後 response 結果。
Notice: use httptest
to start your test server
上面是包含 client 發 request 階段,所以別忘了要先啟動 server,測試才可以正常執行。
1server := httptest.NewServer(handler) // Your test handler
Demo code
1import (
2 "time"
3 "net/http"
4 "net/http/httptest"
5 "net/http/httptrace"
6 "testing"
7)
8
9type Tracer struct {
10 start time.Time
11 end time.Time
12 latency time.Duration
13}
14
15func (l *Tracer) GotConn(connInfo httptrace.GotConnInfo) {
16 l.start = time.Now()
17}
18
19func (l *Tracer) GotFirstResponseByte() {
20 l.end = time.Now()
21 l.latency = l.end.Sub(l.start)
22}
23
24trace := &httptrace.ClientTrace{
25 GotConn: tracer.GotConn,
26 GotFirstResponseByte: tracer.GotFirstResponseByte,
27}
28
29func TestRequest(t *testing.T) {
30 // handler := your handler
31 server := httptest.NewServer(handler) // New test server
32 req, _ := http.NewRequest("POST", server.URL, bytes.NewReader(data))
33 req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
34 res, err := http.DefaultTransport.RoundTrip(req)
35 // check your response
36 server.Close()
37}