- 新增 TraceRecorder 和 TraceSummary,汇总 DNS、连接、TLS、写请求、首包等关键事件 - 为请求执行链接入结构化 trace hooks,补充标准路径与动态路径的 TLS 元信息 - 增加 Request.TraceSummary() 和 Response.TraceSummary(),提供请求级与响应级摘要快照 - 修复共享 TraceRecorder 在 Client 默认选项、Clone 和请求复用场景下的状态串扰问题 - 修复 Response.TraceSummary() 回读 Request 最近状态导致的非快照语义 - 收口自定义 DialFunc 下的 TLS trace 元数据,避免伪造连接地址 - 补充 trace 相关回归测试,覆盖 HTTPS、DNS/Connect、连接复用、共享 recorder、响应快照和自定义拨号场景 - 更新 README,补充 trace、Host 与 TLSServerName 的行为说明
551 lines
14 KiB
Go
551 lines
14 KiB
Go
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
|
||
}
|