114 lines
3.0 KiB
Go
114 lines
3.0 KiB
Go
package common
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"maps"
|
|
"net/http/httptrace"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const UpstreamTimingContextKey = "responses_upstream_timing"
|
|
|
|
// UpstreamTiming contains only transport metadata and request-relative times.
|
|
// HTTP callbacks can run concurrently with streaming and log checkpoints.
|
|
type UpstreamTiming struct {
|
|
mu sync.Mutex
|
|
start time.Time
|
|
events map[string]float64
|
|
attempt int
|
|
channel int
|
|
bodyBytes int64
|
|
reused bool
|
|
gotConn bool
|
|
getConnCount int
|
|
writeCount int
|
|
writeFailed bool
|
|
protocol string
|
|
status int
|
|
}
|
|
|
|
func NewUpstreamTiming(start time.Time, attempt, channel int, bodyBytes int64) *UpstreamTiming {
|
|
if start.IsZero() {
|
|
start = time.Now()
|
|
}
|
|
t := &UpstreamTiming{start: start, events: make(map[string]float64), attempt: attempt, channel: channel, bodyBytes: bodyBytes}
|
|
t.Mark("upstream_start")
|
|
return t
|
|
}
|
|
|
|
func (t *UpstreamTiming) Mark(event string) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if _, exists := t.events[event]; !exists {
|
|
t.events[event] = float64(time.Since(t.start).Microseconds()) / 1000
|
|
}
|
|
}
|
|
|
|
func (t *UpstreamTiming) ClientTrace() *httptrace.ClientTrace {
|
|
return &httptrace.ClientTrace{
|
|
GetConn: func(string) {
|
|
t.Mark("get_connection")
|
|
t.mu.Lock()
|
|
t.getConnCount++
|
|
t.mu.Unlock()
|
|
},
|
|
DNSStart: func(httptrace.DNSStartInfo) { t.Mark("dns_start") },
|
|
DNSDone: func(httptrace.DNSDoneInfo) { t.Mark("dns_done") },
|
|
ConnectStart: func(string, string) { t.Mark("tcp_start") },
|
|
ConnectDone: func(string, string, error) { t.Mark("tcp_done") },
|
|
TLSHandshakeStart: func() { t.Mark("tls_start") },
|
|
TLSHandshakeDone: func(tls.ConnectionState, error) { t.Mark("tls_done") },
|
|
GotConn: func(info httptrace.GotConnInfo) {
|
|
t.Mark("connection_ready")
|
|
t.mu.Lock()
|
|
t.reused, t.gotConn = info.Reused, true
|
|
t.mu.Unlock()
|
|
},
|
|
WroteRequest: func(info httptrace.WroteRequestInfo) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.writeCount++
|
|
t.writeFailed = t.writeFailed || info.Err != nil
|
|
if info.Err == nil {
|
|
t.events["request_sent"] = float64(time.Since(t.start).Microseconds()) / 1000
|
|
}
|
|
},
|
|
GotFirstResponseByte: func() { t.Mark("response_first_byte") },
|
|
}
|
|
}
|
|
|
|
func (t *UpstreamTiming) Response(protocol string, status int) {
|
|
if t == nil {
|
|
return
|
|
}
|
|
t.Mark("response_headers")
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.protocol, t.status = protocol, status
|
|
}
|
|
|
|
func (t *UpstreamTiming) Snapshot() map[string]any {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
events := maps.Clone(t.events)
|
|
result := map[string]any{
|
|
"events_ms": events, "attempt": t.attempt, "channel_id": t.channel,
|
|
"request_body_bytes": t.bodyBytes, "get_connection_count": t.getConnCount,
|
|
"write_count": t.writeCount, "write_failed": t.writeFailed,
|
|
}
|
|
if t.gotConn {
|
|
result["connection_reused"] = t.reused
|
|
}
|
|
if t.protocol != "" {
|
|
result["protocol"], result["status"] = t.protocol, t.status
|
|
}
|
|
return result
|
|
}
|