Initial commit: stats-gateway gRPC service + dashboard + CLI
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package probes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProbeHTTP_Success(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r, err := ProbeHTTP(ctx, "https://example.com/")
|
||||
if err != nil {
|
||||
t.Fatalf("expected success, got err: %v", err)
|
||||
}
|
||||
if r.HTTPLatencyMs <= 0 {
|
||||
t.Errorf("expected positive latency, got %f", r.HTTPLatencyMs)
|
||||
}
|
||||
if r.TLSHandshakeMs <= 0 {
|
||||
t.Errorf("expected positive TLS handshake time, got %f", r.TLSHandshakeMs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeHTTP_Timeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
_, err := ProbeHTTP(ctx, "https://10.255.255.1/") // unreachable
|
||||
if err == nil {
|
||||
t.Fatal("expected timeout error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user