package probes import ( "context" "crypto/tls" "fmt" "net" "net/http" "net/http/httptrace" "time" ) // Result holds timing measurements from a single HTTP probe. type Result struct { Node string // populated by storage on read; ignored by InsertSample (uses separate arg) URL string HTTPLatencyMs float64 TCPRttMs float64 DNSResolveMs float64 TLSHandshakeMs float64 ThroughputKbps int64 Timestamp time.Time } // ProbeHTTP performs an HTTP HEAD request to url, recording timing via httptrace. func ProbeHTTP(ctx context.Context, url string) (*Result, error) { r := &Result{URL: url, Timestamp: time.Now().UTC()} var dnsStart, connStart, tlsStart time.Time trace := &httptrace.ClientTrace{ DNSStart: func(_ httptrace.DNSStartInfo) { dnsStart = time.Now() }, DNSDone: func(_ httptrace.DNSDoneInfo) { if !dnsStart.IsZero() { r.DNSResolveMs = float64(time.Since(dnsStart).Microseconds()) / 1000 } }, ConnectStart: func(_, _ string) { connStart = time.Now() }, ConnectDone: func(_, _ string, _ error) { if !connStart.IsZero() { r.TCPRttMs = float64(time.Since(connStart).Microseconds()) / 1000 } }, TLSHandshakeStart: func() { tlsStart = time.Now() }, TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { if !tlsStart.IsZero() { r.TLSHandshakeMs = float64(time.Since(tlsStart).Microseconds()) / 1000 } }, } req, err := http.NewRequestWithContext(httptrace.WithClientTrace(ctx, trace), "HEAD", url, nil) if err != nil { return nil, fmt.Errorf("new request: %w", err) } client := &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext, }, } start := time.Now() resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("http: %w", err) } defer resp.Body.Close() r.HTTPLatencyMs = float64(time.Since(start).Microseconds()) / 1000 return r, nil }