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  }