// Package probe measures per-phase HTTP request latency. package probe import ( "context" "crypto/tls" "io" "net/http" "net/http/httptrace" "time" ) // Phase holds the measured duration of a single request phase. type Phase struct { Duration time.Duration // Present is false when the phase was skipped (e.g. no TLS for http://). Present bool } // Result holds all timing phases for a single HTTP request. type Result struct { URL string DNS Phase Connect Phase TLS Phase TTFB Phase // server processing: WroteRequest → GotFirstResponseByte Transfer Phase // body read: GotFirstResponseByte → body closed Total Phase // StatusCode is the HTTP response status code (0 on error). StatusCode int // Err is non-nil if the request failed. Err error } // Measure performs an HTTP GET to url and returns a Result with all phases // populated via net/http/httptrace. func Measure(url string) Result { r := Result{URL: url} var ( dnsStart time.Time dnsDone time.Time connectStart time.Time connectDone time.Time tlsStart time.Time tlsDone time.Time wroteRequest time.Time firstByte time.Time ) trace := &httptrace.ClientTrace{ DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() }, DNSDone: func(_ httptrace.DNSDoneInfo) { dnsDone = time.Now() }, ConnectStart: func(_, _ string) { if connectStart.IsZero() { connectStart = time.Now() } }, ConnectDone: func(_, _ string, _ error) { connectDone = time.Now() }, TLSHandshakeStart: func() { tlsStart = time.Now() }, TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { tlsDone = time.Now() }, WroteRequest: func(_ httptrace.WroteRequestInfo) { wroteRequest = time.Now() }, GotFirstResponseByte: func() { firstByte = time.Now() }, } ctx := httptrace.WithClientTrace(context.Background(), trace) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { r.Err = err return r } start := time.Now() resp, err := http.DefaultClient.Do(req) if err != nil { r.Err = err r.Total = Phase{Duration: time.Since(start), Present: true} return r } defer resp.Body.Close() _, err = io.Copy(io.Discard, resp.Body) end := time.Now() r.StatusCode = resp.StatusCode if err != nil { r.Err = err } r.Total = Phase{Duration: end.Sub(start), Present: true} if !dnsStart.IsZero() && !dnsDone.IsZero() { r.DNS = Phase{Duration: dnsDone.Sub(dnsStart), Present: true} } if !connectStart.IsZero() && !connectDone.IsZero() { r.Connect = Phase{Duration: connectDone.Sub(connectStart), Present: true} } if !tlsStart.IsZero() && !tlsDone.IsZero() { r.TLS = Phase{Duration: tlsDone.Sub(tlsStart), Present: true} } if !wroteRequest.IsZero() && !firstByte.IsZero() { r.TTFB = Phase{Duration: firstByte.Sub(wroteRequest), Present: true} } if !firstByte.IsZero() { r.Transfer = Phase{Duration: end.Sub(firstByte), Present: true} } return r }