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 = ©DNS } 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 = ©DNS } 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 }