github.com/qiniu/go-sdk/v7@v7.27.0/internal/uplog/request_uplog.go (about) 1 package uplog 2 3 import ( 4 "crypto/tls" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "net/http/httptrace" 11 "os" 12 "runtime" 13 "strconv" 14 "sync" 15 "sync/atomic" 16 "time" 17 18 "github.com/qiniu/go-sdk/v7/conf" 19 "github.com/qiniu/go-sdk/v7/internal/clientv2" 20 internal_io "github.com/qiniu/go-sdk/v7/internal/io" 21 ) 22 23 type ( 24 GetUpToken func() (string, error) 25 RequestUplog struct { 26 LogType LogType `json:"log_type,omitempty"` 27 StatusCode int `json:"status_code,omitempty"` 28 RequestID string `json:"req_id,omitempty"` 29 Host string `json:"host,omitempty"` 30 Port uint64 `json:"port,omitempty"` 31 RemoteIP string `json:"remote_ip,omitempty"` 32 Method string `json:"method,omitempty"` 33 Path string `json:"path,omitempty"` 34 APIName string `json:"api_name,omitempty"` 35 APIType APIType `json:"api_type,omitempty"` 36 TotalElapsedTime uint64 `json:"total_elapsed_time,omitempty"` 37 DNSElapsedTime uint64 `json:"dns_elapsed_time,omitempty"` 38 ConnectElapsedTime uint64 `json:"connect_elapsed_time,omitempty"` 39 TLSConnectElapsedTime uint64 `json:"tls_connect_elapsed_time,omitempty"` 40 RequestElapsedTime uint64 `json:"request_elapsed_time,omitempty"` 41 WaitElapsedTime uint64 `json:"wait_elapsed_time,omitempty"` 42 ResponseElapsedTime uint64 `json:"response_elapsed_time,omitempty"` 43 BytesTotal int64 `json:"bytes_total,omitempty"` 44 BytesSent int64 `json:"bytes_sent,omitempty"` 45 BytesReceived int64 `json:"bytes_received,omitempty"` 46 TargetBucket string `json:"target_bucket,omitempty"` 47 TargetKey string `json:"target_key,omitempty"` 48 ErrorType ErrorType `json:"error_type,omitempty"` 49 ErrorDescription string `json:"error_description,omitempty"` 50 OSName string `json:"os_name,omitempty"` 51 OSVersion string `json:"os_version,omitempty"` 52 OSArch string `json:"os_arch,omitempty"` 53 SDKName string `json:"sdk_name,omitempty"` 54 SDKVersion string `json:"sdk_version,omitempty"` 55 HTTPClient string `json:"http_client,omitempty"` 56 HTTPClientVersion string `json:"http_client_version,omitempty"` 57 UpTime int64 `json:"up_time,omitempty"` 58 HTTPVersion string `json:"http_version,omitempty"` 59 Pid int `json:"pid,omitempty"` 60 PerceptiveSpeed int64 `json:"perceptive_speed,omitempty"` 61 getUpToken GetUpToken `json:"-"` 62 } 63 64 // callbackTracker manages uplog data updates from concurrent httptrace callbacks 65 // and implements delayed uplog submission with timer reset mechanism 66 callbackTracker struct { 67 uplog *RequestUplog 68 mu sync.RWMutex 69 timer *time.Timer 70 submitted bool 71 submitDelay time.Duration 72 onceSubmit sync.Once 73 getUpToken GetUpToken 74 } 75 ) 76 77 func NewRequestUplog(apiName, targetBucket, targetKey string, getUpToken GetUpToken) (*RequestUplog, error) { 78 return &RequestUplog{ 79 LogType: LogTypeRequest, 80 APIType: APITypeKodo, 81 OSName: truncate(runtime.GOOS, maxFieldValueLength), 82 OSVersion: truncate(getOsVersion(), maxFieldValueLength), 83 OSArch: truncate(runtime.GOARCH, maxFieldValueLength), 84 SDKName: "go", 85 SDKVersion: truncate(conf.Version, maxFieldValueLength), 86 HTTPClient: truncate(getHttpClientName(), maxFieldValueLength), 87 HTTPClientVersion: truncate(conf.Version, maxFieldValueLength), 88 APIName: truncate(apiName, maxFieldValueLength), 89 Pid: os.Getpid(), 90 TargetBucket: truncate(targetBucket, maxFieldValueLength), 91 TargetKey: truncate(targetKey, maxFieldValueLength), 92 getUpToken: getUpToken, 93 }, nil 94 } 95 96 func (uplog *RequestUplog) Priority() clientv2.InterceptorPriority { 97 return clientv2.InterceptorPriorityUplog 98 } 99 100 // newCallbackTracker creates a new tracker for managing uplog updates 101 func newCallbackTracker(uplog *RequestUplog, getUpToken GetUpToken, submitDelay time.Duration) *callbackTracker { 102 return &callbackTracker{ 103 uplog: uplog, 104 getUpToken: getUpToken, 105 submitDelay: submitDelay, 106 } 107 } 108 109 // update safely updates uplog data with read-write lock protection 110 // and resets the submission timer if submission hasn't occurred yet 111 func (t *callbackTracker) update(updateFunc func(*RequestUplog)) { 112 t.mu.Lock() 113 defer t.mu.Unlock() 114 115 // Ignore updates after submission 116 if t.submitted { 117 return 118 } 119 120 // Apply the update 121 updateFunc(t.uplog) 122 123 // Reset the timer 124 if t.timer != nil { 125 t.timer.Stop() 126 } 127 t.timer = time.AfterFunc(t.submitDelay, func() { 128 t.submit() 129 }) 130 } 131 132 // submit triggers the final uplog submission 133 // This method is called either by the timer or explicitly 134 // Once submitted, all subsequent updates are ignored 135 func (t *callbackTracker) submit() { 136 // Ensure timer is cleared after submission to help GC 137 // This defer is outside onceSubmit.Do so it runs every time submit() is called 138 defer func() { 139 t.mu.Lock() 140 t.timer = nil 141 t.mu.Unlock() 142 }() 143 144 t.onceSubmit.Do(func() { 145 t.mu.Lock() 146 t.submitted = true 147 148 // Create a snapshot of the uplog data 149 snapshot := *t.uplog 150 t.mu.Unlock() 151 152 // Serialize and send to uplog channel 153 if uplogBytes, jsonError := json.Marshal(&snapshot); jsonError == nil { 154 uplogChan <- uplogSerializedEntry{ 155 serializedUplog: uplogBytes, 156 getUpToken: t.getUpToken, 157 } 158 } 159 }) 160 } 161 162 func (uplog *RequestUplog) Intercept(req *http.Request, handler clientv2.Handler) (resp *http.Response, err error) { 163 if !IsUplogEnabled() { 164 return handler(req) 165 } 166 167 // Create callback tracker with 2s delay for uplog submission 168 // This allows capturing timing data from async callbacks 169 tracker := newCallbackTracker(uplog, uplog.getUpToken, 2*time.Second) 170 171 var dnsStartTime, gotFirstResponseByteTime, connectStartTime, tlsHandshakeStartTime, wroteHeadersTime, wroteRequestTime time.Time 172 173 uplog.Method = truncate(req.Method, maxFieldValueLength) 174 uplog.Path = truncate(req.URL.Path, maxFieldValueLength) 175 uplog.Host = truncate(req.URL.Hostname(), maxFieldValueLength) 176 if port, err := strconv.ParseUint(req.URL.Port(), 10, 16); err == nil { 177 uplog.Port = port 178 } 179 if req.Body != nil { 180 if kl, ok := req.Body.(internal_io.KnownLength); ok { 181 if detectedLength, err := kl.DetectLength(); err == nil { 182 uplog.BytesTotal = detectedLength 183 } 184 } 185 req.Body = newRequestBodyWithCounter(req.Body, &uplog.BytesSent) 186 } 187 if req.GetBody != nil { 188 getBody := req.GetBody 189 req.GetBody = func() (r io.ReadCloser, err error) { 190 if r, err = getBody(); err != nil { 191 return 192 } 193 r = newRequestBodyWithCounter(r, &uplog.BytesSent) 194 return 195 } 196 } 197 bytesSentBlockLevelTotal := getBytesSentTotalFromContext(req.Context(), true) 198 bytesReceivedBlockLevelTotal := getBytesReceivedTotalFromContext(req.Context(), true) 199 requestsCountBlockLevel := getRequestsCountFromContext(req.Context(), true) 200 bytesSentQualityLevelTotal := getBytesSentTotalFromContext(req.Context(), false) 201 bytesReceivedQualityLevelTotal := getBytesReceivedTotalFromContext(req.Context(), false) 202 requestsCountQualityLevel := getRequestsCountFromContext(req.Context(), false) 203 tracer := httptrace.ClientTrace{ 204 GotConn: func(info httptrace.GotConnInfo) { 205 if host, port, err := net.SplitHostPort(info.Conn.RemoteAddr().String()); err == nil { 206 uplog.RemoteIP = host 207 if uplog.Port == 0 { 208 if portInt, err := strconv.ParseUint(port, 10, 16); err == nil { 209 uplog.Port = portInt 210 } 211 } 212 } 213 }, 214 GotFirstResponseByte: func() { 215 if !wroteRequestTime.IsZero() { 216 tracker.update(func(l *RequestUplog) { 217 l.WaitElapsedTime = getElapsedTime(wroteRequestTime) 218 }) 219 } 220 gotFirstResponseByteTime = time.Now() 221 }, 222 DNSStart: func(httptrace.DNSStartInfo) { 223 dnsStartTime = time.Now() 224 }, 225 DNSDone: func(info httptrace.DNSDoneInfo) { 226 if !dnsStartTime.IsZero() { 227 tracker.update(func(l *RequestUplog) { 228 l.DNSElapsedTime = getElapsedTime(dnsStartTime) 229 }) 230 } 231 }, 232 ConnectStart: func(network string, addr string) { 233 connectStartTime = time.Now() 234 }, 235 ConnectDone: func(network string, addr string, err error) { 236 if !connectStartTime.IsZero() { 237 tracker.update(func(l *RequestUplog) { 238 l.ConnectElapsedTime = getElapsedTime(connectStartTime) 239 }) 240 } 241 }, 242 TLSHandshakeStart: func() { 243 tlsHandshakeStartTime = time.Now() 244 }, 245 TLSHandshakeDone: func(tls.ConnectionState, error) { 246 if !tlsHandshakeStartTime.IsZero() { 247 tracker.update(func(l *RequestUplog) { 248 l.TLSConnectElapsedTime = getElapsedTime(tlsHandshakeStartTime) 249 }) 250 } 251 }, 252 WroteHeaders: func() { 253 wroteHeadersTime = time.Now() 254 }, 255 WroteRequest: func(info httptrace.WroteRequestInfo) { 256 if !wroteHeadersTime.IsZero() { 257 tracker.update(func(l *RequestUplog) { 258 l.RequestElapsedTime = getElapsedTime(wroteHeadersTime) 259 }) 260 } 261 wroteRequestTime = time.Now() 262 }, 263 } 264 req = req.WithContext(httptrace.WithClientTrace(req.Context(), &tracer)) 265 beginAt := time.Now() 266 uplog.UpTime = beginAt.Unix() 267 resp, err = handler(req) 268 if !gotFirstResponseByteTime.IsZero() { 269 tracker.update(func(l *RequestUplog) { 270 l.ResponseElapsedTime = getElapsedTime(gotFirstResponseByteTime) 271 }) 272 } 273 tracker.update(func(l *RequestUplog) { 274 l.TotalElapsedTime = getElapsedTime(beginAt) 275 }) 276 if err != nil { 277 uplog.ErrorType, uplog.ErrorDescription = uplog.detect(resp, err) 278 uplog.ErrorDescription = truncate(uplog.ErrorDescription, maxFieldValueLength) 279 } 280 addCounter(bytesSentBlockLevelTotal, uint64(uplog.BytesSent)) 281 addCounter(bytesSentQualityLevelTotal, uint64(uplog.BytesSent)) 282 addCounter(requestsCountBlockLevel, 1) 283 addCounter(requestsCountQualityLevel, 1) 284 if resp != nil { 285 uplog.StatusCode = resp.StatusCode 286 uplog.RequestID = resp.Header.Get("x-reqid") 287 if resp.Body != nil { 288 if kl, ok := resp.Body.(internal_io.KnownLength); ok { 289 if detectedLength, err := kl.DetectLength(); err == nil { 290 uplog.BytesReceived = detectedLength 291 addCounter(bytesReceivedBlockLevelTotal, uint64(detectedLength)) 292 addCounter(bytesReceivedQualityLevelTotal, uint64(detectedLength)) 293 } 294 } 295 } 296 if resp.Request != nil { 297 if resp.Request.ProtoMajor == 1 || resp.Request.ProtoMinor != 0 { 298 uplog.HTTPVersion = fmt.Sprintf("%d.%d", resp.Request.ProtoMajor, resp.Request.ProtoMinor) 299 } else { 300 uplog.HTTPVersion = fmt.Sprintf("%d", resp.Request.ProtoMajor) 301 } 302 } 303 } 304 if uplog.TotalElapsedTime > 0 { 305 if uplog.BytesSent > uplog.BytesReceived { 306 uplog.PerceptiveSpeed = uplog.BytesSent * 1000 / int64(uplog.TotalElapsedTime) 307 } else { 308 uplog.PerceptiveSpeed = uplog.BytesReceived * 1000 / int64(uplog.TotalElapsedTime) 309 } 310 } 311 312 // Trigger uplog submission 313 // The timer will fire after delay unless more updates arrive 314 // Note: We allow the last timer to keep running to capture any late async callbacks 315 // Once submit() is called, no new timers will be created (checked in update()), 316 // allowing the tracker to be garbage collected after the timer fires. 317 tracker.submit() 318 319 return 320 } 321 322 func (uplog *RequestUplog) detect(response *http.Response, err error) (errorType ErrorType, errorDescription string) { 323 if err == nil { 324 if response.StatusCode >= 300 { 325 errorType = ErrorTypeResponseError 326 errorDescription = response.Status 327 } 328 return 329 } 330 errorType = detectErrorType(err) 331 errorDescription = err.Error() 332 return 333 } 334 335 type requestBodyWithCounter struct { 336 r io.ReadCloser 337 c *int64 338 } 339 340 func newRequestBodyWithCounter(r io.ReadCloser, c *int64) *requestBodyWithCounter { 341 *c = 0 342 return &requestBodyWithCounter{r, c} 343 } 344 345 func (r *requestBodyWithCounter) Read(p []byte) (n int, err error) { 346 n, err = r.r.Read(p) 347 *r.c += int64(n) 348 return 349 } 350 351 func (r *requestBodyWithCounter) Close() error { 352 return r.r.Close() 353 } 354 355 func getElapsedTime(startTime time.Time) uint64 { 356 return uint64(time.Since(startTime) / time.Millisecond) 357 } 358 359 func addCounter(c *uint64, n uint64) { 360 if c != nil { 361 atomic.AddUint64(c, n) 362 } 363 }