Files
tsnet-proxy/vendor/github.com/tcnksm/go-httpstat/httpstat.go
2024-11-01 17:43:06 +00:00

148 lines
4.2 KiB
Go

// Package httpstat traces HTTP latency infomation (DNSLookup, TCP Connection and so on) on any golang HTTP request.
// It uses `httptrace` package. Just create `go-httpstat` powered `context.Context` and give it your `http.Request` (no big code modification is required).
package httpstat
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"time"
)
// Result stores httpstat info.
type Result struct {
// The following are duration for each phase
DNSLookup time.Duration
TCPConnection time.Duration
TLSHandshake time.Duration
ServerProcessing time.Duration
contentTransfer time.Duration
// 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
dnsDone time.Time
tcpStart time.Time
tcpDone time.Time
tlsStart time.Time
tlsDone time.Time
serverStart time.Time
serverDone time.Time
transferStart time.Time
trasferDone time.Time // need to be provided from outside
// isTLS is true when connection seems to use TLS
isTLS bool
// isReused is true when connection is reused (keep-alive)
isReused bool
}
func (r *Result) durations() map[string]time.Duration {
return map[string]time.Duration{
"DNSLookup": r.DNSLookup,
"TCPConnection": r.TCPConnection,
"TLSHandshake": r.TLSHandshake,
"ServerProcessing": r.ServerProcessing,
"ContentTransfer": r.contentTransfer,
"NameLookup": r.NameLookup,
"Connect": r.Connect,
"Pretransfer": r.Connect,
"StartTransfer": r.StartTransfer,
"Total": r.total,
}
}
// ContentTransfer returns the duration of content transfer time.
// It is from first response byte to the given time. The time must
// be time after read body (go-httpstat can not detect that time).
func (r *Result) ContentTransfer(t time.Time) time.Duration {
return t.Sub(r.t4)
}
// Total returns the duration of total http request.
// 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.t0)
}
// Format formats stats result.
func (r Result) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
var buf bytes.Buffer
fmt.Fprintf(&buf, "DNS lookup: %4d ms\n",
int(r.DNSLookup/time.Millisecond))
fmt.Fprintf(&buf, "TCP connection: %4d ms\n",
int(r.TCPConnection/time.Millisecond))
fmt.Fprintf(&buf, "TLS handshake: %4d ms\n",
int(r.TLSHandshake/time.Millisecond))
fmt.Fprintf(&buf, "Server processing: %4d ms\n",
int(r.ServerProcessing/time.Millisecond))
if !r.t5.IsZero() {
fmt.Fprintf(&buf, "Content transfer: %4d ms\n\n",
int(r.contentTransfer/time.Millisecond))
} else {
fmt.Fprintf(&buf, "Content transfer: %4s ms\n\n", "-")
}
fmt.Fprintf(&buf, "Name Lookup: %4d ms\n",
int(r.NameLookup/time.Millisecond))
fmt.Fprintf(&buf, "Connect: %4d ms\n",
int(r.Connect/time.Millisecond))
fmt.Fprintf(&buf, "Pre Transfer: %4d ms\n",
int(r.Pretransfer/time.Millisecond))
fmt.Fprintf(&buf, "Start Transfer: %4d ms\n",
int(r.StartTransfer/time.Millisecond))
if !r.t5.IsZero() {
fmt.Fprintf(&buf, "Total: %4d ms\n",
int(r.total/time.Millisecond))
} else {
fmt.Fprintf(&buf, "Total: %4s ms\n", "-")
}
io.WriteString(s, buf.String())
return
}
fallthrough
case 's', 'q':
d := r.durations()
list := make([]string, 0, len(d))
for k, v := range d {
// Handle when End function is not called
if (k == "ContentTransfer" || k == "Total") && r.t5.IsZero() {
list = append(list, fmt.Sprintf("%s: - ms", k))
continue
}
list = append(list, fmt.Sprintf("%s: %d ms", k, v/time.Millisecond))
}
io.WriteString(s, strings.Join(list, ", "))
}
}
// WithHTTPStat is a wrapper of httptrace.WithClientTrace. It records the
// time of each httptrace hooks.
func WithHTTPStat(ctx context.Context, r *Result) context.Context {
return withClientTrace(ctx, r)
}