Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"os"
"time"

"github.com/tcnksm/go-httpstat"
"github.com/inngest/go-httpstat"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"net/http"
"time"

"github.com/tcnksm/go-httpstat"
"github.com/inngest/go-httpstat"
)

func Example() {
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/inngest/go-httpstat

go 1.23.2
Empty file added go.sum
Empty file.
59 changes: 56 additions & 3 deletions go18.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// +build go1.8

package httpstat

import (
"context"
"crypto/tls"
"net"
"net/http/httptrace"
"strings"
"time"
)

Expand Down Expand Up @@ -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)
Expand All @@ -62,33 +75,63 @@ 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)
r.Connect = r.tcpDone.Sub(r.dnsStart)
},

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)
r.Pretransfer = r.tlsDone.Sub(r.dnsStart)
},

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`
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
23 changes: 16 additions & 7 deletions httpstat.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,35 @@ 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
TLSHandshake time.Duration
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
Pretransfer time.Duration
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
Expand All @@ -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,
Expand All @@ -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('+') {
Expand Down Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions httpstat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"testing"
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down