diff --git a/_example/main.go b/_example/main.go index 6012092..de27842 100644 --- a/_example/main.go +++ b/_example/main.go @@ -9,7 +9,7 @@ import ( "os" "time" - "github.com/tcnksm/go-httpstat" + "github.com/inngest/go-httpstat" ) func main() { diff --git a/example_test.go b/example_test.go index fb27f02..03435b8 100644 --- a/example_test.go +++ b/example_test.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/tcnksm/go-httpstat" + "github.com/inngest/go-httpstat" ) func Example() { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1aab485 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/inngest/go-httpstat + +go 1.23.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/go18.go b/go18.go index 3b5df3c..0d8109a 100644 --- a/go18.go +++ b/go18.go @@ -1,11 +1,11 @@ -// +build go1.8 - package httpstat import ( "context" "crypto/tls" + "net" "net/http/httptrace" + "strings" "time" ) @@ -35,23 +35,36 @@ func (r *Result) ContentTransfer(t time.Time) time.Duration { // It is from dns lookup start time to the given time. The // time must be time after read body (go-httpstat can not detect that time). func (r *Result) Total(t time.Time) time.Duration { - return t.Sub(r.dnsStart) + return r.total } func withClientTrace(ctx context.Context, r *Result) context.Context { return httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ DNSStart: func(i httptrace.DNSStartInfo) { + r.mutex.Lock() + defer r.mutex.Unlock() r.dnsStart = time.Now() }, DNSDone: func(i httptrace.DNSDoneInfo) { + r.mutex.Lock() + defer r.mutex.Unlock() r.dnsDone = time.Now() r.DNSLookup = r.dnsDone.Sub(r.dnsStart) r.NameLookup = r.dnsDone.Sub(r.dnsStart) + + for _, ip := range i.Addrs { + if IsIPv6(ip.IP.String()) { + r.IsIPv6 = true + } + } + r.Addresses = i.Addrs }, ConnectStart: func(_, _ string) { + r.mutex.Lock() + defer r.mutex.Unlock() r.tcpStart = time.Now() // When connecting to IP (When no DNS lookup) @@ -62,6 +75,8 @@ func withClientTrace(ctx context.Context, r *Result) context.Context { }, ConnectDone: func(network, addr string, err error) { + r.mutex.Lock() + defer r.mutex.Unlock() r.tcpDone = time.Now() r.TCPConnection = r.tcpDone.Sub(r.tcpStart) @@ -69,11 +84,15 @@ func withClientTrace(ctx context.Context, r *Result) context.Context { }, TLSHandshakeStart: func() { + r.mutex.Lock() + defer r.mutex.Unlock() r.isTLS = true r.tlsStart = time.Now() }, TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { + r.mutex.Lock() + defer r.mutex.Unlock() r.tlsDone = time.Now() r.TLSHandshake = r.tlsDone.Sub(r.tlsStart) @@ -81,14 +100,38 @@ func withClientTrace(ctx context.Context, r *Result) context.Context { }, GotConn: func(i httptrace.GotConnInfo) { + r.mutex.Lock() + defer r.mutex.Unlock() // Handle when keep alive is used and connection is reused. // DNSStart(Done) and ConnectStart(Done) is skipped if i.Reused { r.isReused = true } + + switch addr := i.Conn.RemoteAddr().(type) { + case *net.TCPAddr: + r.ConnectedTo = ConnectedTo{ + IP: addr.IP.String(), + Port: addr.Port, + Zone: addr.Zone, + } + case *net.UDPAddr: + r.ConnectedTo = ConnectedTo{ + IP: addr.IP.String(), + Port: addr.Port, + Zone: addr.Zone, + } + case *net.IPAddr: + r.ConnectedTo = ConnectedTo{ + IP: addr.IP.String(), + Zone: addr.Zone, + } + } }, WroteRequest: func(info httptrace.WroteRequestInfo) { + r.mutex.Lock() + defer r.mutex.Unlock() r.serverStart = time.Now() // When client doesn't use DialContext or using old (before go1.7) `net` @@ -123,6 +166,8 @@ func withClientTrace(ctx context.Context, r *Result) context.Context { }, GotFirstResponseByte: func() { + r.mutex.Lock() + defer r.mutex.Unlock() r.serverDone = time.Now() r.ServerProcessing = r.serverDone.Sub(r.serverStart) @@ -132,3 +177,11 @@ func withClientTrace(ctx context.Context, r *Result) context.Context { }, }) } + +func IsIPv4(address string) bool { + return strings.Count(address, ":") < 2 +} + +func IsIPv6(address string) bool { + return strings.Count(address, ":") >= 2 +} diff --git a/httpstat.go b/httpstat.go index 15ce78c..d2055e9 100644 --- a/httpstat.go +++ b/httpstat.go @@ -7,12 +7,16 @@ import ( "context" "fmt" "io" + "net" "strings" + "sync" "time" ) // Result stores httpstat info. type Result struct { + mutex sync.Mutex + // The following are duration for each phase DNSLookup time.Duration TCPConnection time.Duration @@ -20,6 +24,11 @@ type Result struct { ServerProcessing time.Duration contentTransfer time.Duration + // DNS and connection Info + IsIPv6 bool + Addresses []net.IPAddr + ConnectedTo ConnectedTo + // The followings are timeline of request NameLookup time.Duration Connect time.Duration @@ -27,11 +36,6 @@ type Result struct { StartTransfer time.Duration total time.Duration - t0 time.Time - t1 time.Time - t2 time.Time - t3 time.Time - t4 time.Time t5 time.Time // need to be provided from outside dnsStart time.Time @@ -52,6 +56,12 @@ type Result struct { isReused bool } +type ConnectedTo struct { + IP string + Port int + Zone string +} + func (r *Result) durations() map[string]time.Duration { return map[string]time.Duration{ "DNSLookup": r.DNSLookup, @@ -69,7 +79,7 @@ func (r *Result) durations() map[string]time.Duration { } // Format formats stats result. -func (r Result) Format(s fmt.State, verb rune) { +func (r *Result) Format(s fmt.State, verb rune) { switch verb { case 'v': if s.Flag('+') { @@ -123,7 +133,6 @@ func (r Result) Format(s fmt.State, verb rune) { } io.WriteString(s, strings.Join(list, ", ")) } - } // WithHTTPStat is a wrapper of httptrace.WithClientTrace. It records the diff --git a/httpstat_test.go b/httpstat_test.go index 6f3f812..9b7184e 100644 --- a/httpstat_test.go +++ b/httpstat_test.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "net" "net/http" "testing" @@ -58,7 +57,7 @@ func TestHTTPStat_HTTPS(t *testing.T) { t.Fatal("client.Do failed:", err) } - if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { + if _, err := io.Copy(io.Discard, res.Body); err != nil { t.Fatal("io.Copy failed:", err) } res.Body.Close() @@ -85,7 +84,7 @@ func TestHTTPStat_HTTP(t *testing.T) { t.Fatal("client.Do failed:", err) } - if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { + if _, err := io.Copy(io.Discard, res.Body); err != nil { t.Fatal("io.Copy failed:", err) } res.Body.Close() @@ -122,7 +121,7 @@ func TestHTTPStat_KeepAlive(t *testing.T) { t.Fatal("Request failed:", err) } - if _, err := io.Copy(ioutil.Discard, res1.Body); err != nil { + if _, err := io.Copy(io.Discard, res1.Body); err != nil { t.Fatal("Copy body failed:", err) } res1.Body.Close() @@ -136,7 +135,7 @@ func TestHTTPStat_KeepAlive(t *testing.T) { t.Fatal("Request failed:", err) } - if _, err := io.Copy(ioutil.Discard, res2.Body); err != nil { + if _, err := io.Copy(io.Discard, res2.Body); err != nil { t.Fatal("Copy body failed:", err) } res2.Body.Close() @@ -180,7 +179,7 @@ func TestHTTPStat_beforeGO17(t *testing.T) { t.Fatal("client.Do failed:", err) } - if _, err := io.Copy(ioutil.Discard, res.Body); err != nil { + if _, err := io.Copy(io.Discard, res.Body); err != nil { t.Fatal("io.Copy failed:", err) } res.Body.Close() @@ -244,7 +243,7 @@ Start Transfer: 100 ms Total: 100 ms ` var buf bytes.Buffer - fmt.Fprintf(&buf, "%+v", result) + fmt.Fprintf(&buf, "%+v", &result) if got := buf.String(); want != got { t.Fatalf("expect to be eq:\n\nwant:\n\n%s\ngot:\n\n%s\n", want, got) }