starnet/trace_summary.go

551 lines
14 KiB
Go
Raw Permalink Normal View History

package starnet
import (
"crypto/tls"
"net"
"net/http"
"sync"
"time"
)
// TraceSummary 是一次请求执行的 trace 摘要。
type TraceSummary struct {
Method string
URL string
StartedAt time.Time
ResponseAt time.Time
StatusCode int
ResponseProto string
Conn TraceConnSummary
DNS *TraceDNSSummary
DNSEvents []TraceDNSSummary
Connect []TraceConnectSummary
TLS *TraceTLSSummary
RequestWrittenAt time.Time
RequestWriteErr error
FirstResponseByteAt time.Time
}
// TraceConnSummary 是连接复用与套接字信息摘要。
type TraceConnSummary struct {
Addr string
LocalAddr string
RemoteAddr string
Reused bool
WasIdle bool
IdleTime time.Duration
}
// TraceDNSSummary 是 DNS 解析摘要。
type TraceDNSSummary struct {
Host string
Addrs []string
Coalesced bool
StartedAt time.Time
CompletedAt time.Time
Duration time.Duration
Err error
}
// TraceConnectSummary 是单次建连尝试摘要。
type TraceConnectSummary struct {
Network string
Addr string
StartedAt time.Time
CompletedAt time.Time
Duration time.Duration
Err error
}
// TraceTLSSummary 是 TLS 握手与连接状态摘要。
type TraceTLSSummary struct {
Network string
Addr string
ServerName string
Version uint16
VersionName string
CipherSuite uint16
CipherSuiteName string
CurveID tls.CurveID
CurveName string
NegotiatedProtocol string
DidResume bool
ECHAccepted bool
VerifiedChains int
StartedAt time.Time
CompletedAt time.Time
Duration time.Duration
Err error
PeerCertificates []TraceCertificateSummary
}
// TraceCertificateSummary 是单张证书的关键信息摘要。
type TraceCertificateSummary struct {
Subject string
Issuer string
DNSNames []string
IPAddresses []string
}
// TraceRecorder 聚合最近一次发布的 trace 摘要。
// 通过 Request/Client 绑定时starnet 会为每次执行创建私有运行态并在完成后发布摘要;
// 直接使用 Hooks() 时,调用方仍需自行管理 Reset 与生命周期。
type TraceRecorder struct {
mu sync.Mutex
summary TraceSummary
pendingDNS []TraceDNSSummary
pendingConnectStarts map[string][]time.Time
pendingTLSStart time.Time
hooks *TraceHooks
}
// NewTraceRecorder 创建请求级 trace 记录器。
func NewTraceRecorder() *TraceRecorder {
recorder := &TraceRecorder{}
recorder.hooks = &TraceHooks{
GetConn: recorder.onGetConn,
GotConn: recorder.onGotConn,
DNSStart: recorder.onDNSStart,
DNSDone: recorder.onDNSDone,
ConnectStart: recorder.onConnectStart,
ConnectDone: recorder.onConnectDone,
TLSHandshakeStart: recorder.onTLSHandshakeStart,
TLSHandshakeDone: recorder.onTLSHandshakeDone,
WroteRequest: recorder.onWroteRequest,
GotFirstResponseByte: recorder.onGotFirstResponseByte,
}
return recorder
}
// Hooks 返回可挂到请求上的底层 trace hooks。
func (r *TraceRecorder) Hooks() *TraceHooks {
if r == nil {
return nil
}
if r.hooks == nil {
r.hooks = &TraceHooks{
GetConn: r.onGetConn,
GotConn: r.onGotConn,
DNSStart: r.onDNSStart,
DNSDone: r.onDNSDone,
ConnectStart: r.onConnectStart,
ConnectDone: r.onConnectDone,
TLSHandshakeStart: r.onTLSHandshakeStart,
TLSHandshakeDone: r.onTLSHandshakeDone,
WroteRequest: r.onWroteRequest,
GotFirstResponseByte: r.onGotFirstResponseByte,
}
}
return r.hooks
}
// Reset 清空当前摘要和内部状态。
func (r *TraceRecorder) Reset() {
if r == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.resetLocked()
}
// Summary 返回当前 trace 摘要的快照。
func (r *TraceRecorder) Summary() TraceSummary {
if r == nil {
return TraceSummary{}
}
r.mu.Lock()
defer r.mu.Unlock()
return cloneTraceSummary(r.summary)
}
func (r *TraceRecorder) forkExecution() *TraceRecorder {
if r == nil {
return nil
}
return NewTraceRecorder()
}
func (r *TraceRecorder) publishSummary(summary TraceSummary) {
if r == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.summary = cloneTraceSummary(summary)
}
func (r *TraceRecorder) startRequest() {
if r == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.resetLocked()
r.summary.StartedAt = time.Now()
}
func (r *TraceRecorder) observePreparedRequest(req *http.Request) {
if r == nil || req == nil || req.URL == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
r.ensureStartedLocked(time.Now())
r.summary.Method = req.Method
r.summary.URL = req.URL.String()
}
func (r *TraceRecorder) observeResponse(resp *http.Response) {
if r == nil || resp == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
r.summary.ResponseAt = now
r.summary.StatusCode = resp.StatusCode
r.summary.ResponseProto = resp.Proto
if resp.TLS != nil {
r.summary.TLS = mergeTraceTLSSummary(r.summary.TLS, *resp.TLS)
}
}
func (r *TraceRecorder) ensureStartedLocked(now time.Time) {
if r.summary.StartedAt.IsZero() {
r.summary.StartedAt = now
}
}
func (r *TraceRecorder) resetLocked() {
r.summary = TraceSummary{}
r.pendingDNS = nil
r.pendingTLSStart = time.Time{}
if len(r.pendingConnectStarts) == 0 {
r.pendingConnectStarts = nil
return
}
for key := range r.pendingConnectStarts {
delete(r.pendingConnectStarts, key)
}
r.pendingConnectStarts = nil
}
func (r *TraceRecorder) onGetConn(info TraceGetConnInfo) {
r.mu.Lock()
defer r.mu.Unlock()
r.ensureStartedLocked(time.Now())
r.summary.Conn.Addr = info.Addr
}
func (r *TraceRecorder) onGotConn(info TraceGotConnInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
r.summary.Conn.Reused = info.Reused
r.summary.Conn.WasIdle = info.WasIdle
r.summary.Conn.IdleTime = info.IdleTime
if info.Conn != nil {
r.summary.Conn.LocalAddr = traceAddrString(info.Conn.LocalAddr())
r.summary.Conn.RemoteAddr = traceAddrString(info.Conn.RemoteAddr())
}
}
func (r *TraceRecorder) onDNSStart(info TraceDNSStartInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
dns := TraceDNSSummary{
Host: info.Host,
StartedAt: now,
}
r.pendingDNS = append(r.pendingDNS, dns)
copyDNS := dns
r.summary.DNS = &copyDNS
}
func (r *TraceRecorder) onDNSDone(info TraceDNSDoneInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
dns := TraceDNSSummary{
Host: "",
Addrs: traceIPAddrsToStrings(info.Addrs),
Coalesced: info.Coalesced,
CompletedAt: now,
Err: info.Err,
}
if len(r.pendingDNS) > 0 {
dns.Host = r.pendingDNS[0].Host
dns.StartedAt = r.pendingDNS[0].StartedAt
if len(r.pendingDNS) == 1 {
r.pendingDNS = nil
} else {
r.pendingDNS = append([]TraceDNSSummary(nil), r.pendingDNS[1:]...)
}
} else if r.summary.DNS != nil {
dns.Host = r.summary.DNS.Host
dns.StartedAt = r.summary.DNS.StartedAt
}
if !dns.StartedAt.IsZero() {
dns.Duration = now.Sub(dns.StartedAt)
}
r.summary.DNSEvents = append(r.summary.DNSEvents, dns)
copyDNS := dns
r.summary.DNS = &copyDNS
}
func (r *TraceRecorder) onConnectStart(info TraceConnectStartInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
if r.pendingConnectStarts == nil {
r.pendingConnectStarts = make(map[string][]time.Time)
}
key := traceConnectKey(info.Network, info.Addr)
r.pendingConnectStarts[key] = append(r.pendingConnectStarts[key], now)
r.summary.Connect = append(r.summary.Connect, TraceConnectSummary{
Network: info.Network,
Addr: info.Addr,
StartedAt: now,
})
}
func (r *TraceRecorder) onConnectDone(info TraceConnectDoneInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
start := time.Time{}
key := traceConnectKey(info.Network, info.Addr)
if starts := r.pendingConnectStarts[key]; len(starts) > 0 {
start = starts[0]
if len(starts) == 1 {
delete(r.pendingConnectStarts, key)
} else {
r.pendingConnectStarts[key] = starts[1:]
}
}
connect := TraceConnectSummary{
Network: info.Network,
Addr: info.Addr,
StartedAt: start,
CompletedAt: now,
Err: info.Err,
}
if !start.IsZero() {
connect.Duration = now.Sub(start)
}
for index := len(r.summary.Connect) - 1; index >= 0; index-- {
item := &r.summary.Connect[index]
if item.Network != info.Network || item.Addr != info.Addr || !item.CompletedAt.IsZero() {
continue
}
item.CompletedAt = now
item.Duration = connect.Duration
item.Err = info.Err
return
}
r.summary.Connect = append(r.summary.Connect, connect)
}
func (r *TraceRecorder) onTLSHandshakeStart(info TraceTLSHandshakeStartInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
r.pendingTLSStart = now
r.summary.TLS = &TraceTLSSummary{
Network: info.Network,
Addr: info.Addr,
ServerName: info.ServerName,
StartedAt: now,
}
}
func (r *TraceRecorder) onTLSHandshakeDone(info TraceTLSHandshakeDoneInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
var tlsSummary *TraceTLSSummary
if r.summary.TLS != nil {
copied := *r.summary.TLS
tlsSummary = &copied
} else {
tlsSummary = &TraceTLSSummary{}
}
if tlsSummary.Network == "" {
tlsSummary.Network = info.Network
}
if tlsSummary.Addr == "" {
tlsSummary.Addr = info.Addr
}
if tlsSummary.ServerName == "" {
tlsSummary.ServerName = info.ServerName
}
if tlsSummary.StartedAt.IsZero() {
tlsSummary.StartedAt = r.pendingTLSStart
}
tlsSummary.CompletedAt = now
if !tlsSummary.StartedAt.IsZero() {
tlsSummary.Duration = now.Sub(tlsSummary.StartedAt)
}
tlsSummary.Err = info.Err
tlsSummary = mergeTraceTLSSummary(tlsSummary, info.ConnectionState)
if tlsSummary.ServerName == "" {
tlsSummary.ServerName = info.ServerName
}
if tlsSummary.Addr == "" {
tlsSummary.Addr = info.Addr
}
if tlsSummary.Network == "" {
tlsSummary.Network = info.Network
}
r.pendingTLSStart = time.Time{}
r.summary.TLS = tlsSummary
}
func (r *TraceRecorder) onWroteRequest(info TraceWroteRequestInfo) {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
r.summary.RequestWrittenAt = now
r.summary.RequestWriteErr = info.Err
}
func (r *TraceRecorder) onGotFirstResponseByte() {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
r.ensureStartedLocked(now)
r.summary.FirstResponseByteAt = now
}
func traceAddrString(addr net.Addr) string {
if addr == nil {
return ""
}
return addr.String()
}
func traceConnectKey(network, addr string) string {
return network + "\x00" + addr
}
func traceIPAddrsToStrings(addrs []net.IPAddr) []string {
if len(addrs) == 0 {
return nil
}
out := make([]string, 0, len(addrs))
for _, addr := range addrs {
out = append(out, addr.String())
}
return out
}
func mergeTraceTLSSummary(summary *TraceTLSSummary, state tls.ConnectionState) *TraceTLSSummary {
if summary == nil {
summary = &TraceTLSSummary{}
}
if state.Version != 0 {
summary.Version = state.Version
summary.VersionName = tls.VersionName(state.Version)
}
if state.CipherSuite != 0 {
summary.CipherSuite = state.CipherSuite
summary.CipherSuiteName = tls.CipherSuiteName(state.CipherSuite)
}
if state.CurveID != 0 {
summary.CurveID = state.CurveID
summary.CurveName = state.CurveID.String()
}
if state.ServerName != "" {
summary.ServerName = state.ServerName
}
if state.NegotiatedProtocol != "" {
summary.NegotiatedProtocol = state.NegotiatedProtocol
}
summary.DidResume = state.DidResume
summary.ECHAccepted = state.ECHAccepted
summary.VerifiedChains = len(state.VerifiedChains)
if len(state.PeerCertificates) > 0 {
summary.PeerCertificates = make([]TraceCertificateSummary, 0, len(state.PeerCertificates))
for _, cert := range state.PeerCertificates {
certSummary := TraceCertificateSummary{
Subject: cert.Subject.String(),
Issuer: cert.Issuer.String(),
DNSNames: append([]string(nil), cert.DNSNames...),
}
if len(cert.IPAddresses) > 0 {
certSummary.IPAddresses = make([]string, 0, len(cert.IPAddresses))
for _, ip := range cert.IPAddresses {
certSummary.IPAddresses = append(certSummary.IPAddresses, ip.String())
}
}
summary.PeerCertificates = append(summary.PeerCertificates, certSummary)
}
}
return summary
}
func cloneTraceSummary(summary TraceSummary) TraceSummary {
cloned := summary
if summary.DNS != nil {
dns := *summary.DNS
dns.Addrs = append([]string(nil), summary.DNS.Addrs...)
cloned.DNS = &dns
}
if len(summary.DNSEvents) > 0 {
cloned.DNSEvents = make([]TraceDNSSummary, 0, len(summary.DNSEvents))
for _, dns := range summary.DNSEvents {
cloned.DNSEvents = append(cloned.DNSEvents, TraceDNSSummary{
Host: dns.Host,
Addrs: append([]string(nil), dns.Addrs...),
Coalesced: dns.Coalesced,
StartedAt: dns.StartedAt,
CompletedAt: dns.CompletedAt,
Duration: dns.Duration,
Err: dns.Err,
})
}
}
if len(summary.Connect) > 0 {
cloned.Connect = append([]TraceConnectSummary(nil), summary.Connect...)
}
if summary.TLS != nil {
tlsSummary := *summary.TLS
if len(summary.TLS.PeerCertificates) > 0 {
tlsSummary.PeerCertificates = make([]TraceCertificateSummary, 0, len(summary.TLS.PeerCertificates))
for _, cert := range summary.TLS.PeerCertificates {
tlsSummary.PeerCertificates = append(tlsSummary.PeerCertificates, TraceCertificateSummary{
Subject: cert.Subject,
Issuer: cert.Issuer,
DNSNames: append([]string(nil), cert.DNSNames...),
IPAddresses: append([]string(nil), cert.IPAddresses...),
})
}
}
cloned.TLS = &tlsSummary
}
return cloned
}
func cloneTraceSummaryPtr(summary TraceSummary) *TraceSummary {
cloned := cloneTraceSummary(summary)
return &cloned
}