git-lfs/lfshttp/stats.go
brian m. carlson 087db1de70
Set package version to v3
Since we're about to do a v3.0.0 release, let's bump the version to v3.

Make this change automatically with the following command to avoid any
missed items:

  git grep -l github.com/git-lfs/git-lfs/v2 | \
  xargs sed -i -e 's!github.com/git-lfs/git-lfs/v2!github.com/git-lfs/git-lfs/v3!g'
2021-09-02 20:41:08 +00:00

172 lines
4.2 KiB
Go

package lfshttp
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/git-lfs/git-lfs/v3/tools"
)
type httpTransfer struct {
// members managed via sync/atomic must be aligned at the top of this
// structure (see: https://github.com/git-lfs/git-lfs/pull/2880).
RequestBodySize int64
Start int64
ResponseStart int64
ConnStart int64
ConnEnd int64
DNSStart int64
DNSEnd int64
TLSStart int64
TLSEnd int64
URL string
Method string
Key string
}
type statsContextKey string
const transferKey = statsContextKey("transfer")
func (c *Client) LogHTTPStats(w io.WriteCloser) {
fmt.Fprintf(w, "concurrent=%d time=%d version=%s\n", c.ConcurrentTransfers, time.Now().Unix(), UserAgent)
c.httpLogger = newSyncLogger(w)
}
// LogStats is intended to be called after all HTTP operations for the
// command have finished. It dumps k/v logs, one line per httpTransfer into
// a log file with the current timestamp.
//
// DEPRECATED: Call LogHTTPStats() before the first HTTP request.
func (c *Client) LogStats(out io.Writer) {}
// LogRequest tells the client to log the request's stats to the http log
// after the response body has been read.
func (c *Client) LogRequest(r *http.Request, reqKey string) *http.Request {
if c.httpLogger == nil {
return r
}
t := &httpTransfer{
URL: strings.SplitN(r.URL.String(), "?", 2)[0],
Method: r.Method,
Key: reqKey,
}
ctx := httptrace.WithClientTrace(r.Context(), &httptrace.ClientTrace{
GetConn: func(_ string) {
atomic.CompareAndSwapInt64(&t.Start, 0, time.Now().UnixNano())
},
DNSStart: func(_ httptrace.DNSStartInfo) {
atomic.CompareAndSwapInt64(&t.DNSStart, 0, time.Now().UnixNano())
},
DNSDone: func(_ httptrace.DNSDoneInfo) {
atomic.CompareAndSwapInt64(&t.DNSEnd, 0, time.Now().UnixNano())
},
ConnectStart: func(_, _ string) {
atomic.CompareAndSwapInt64(&t.ConnStart, 0, time.Now().UnixNano())
},
ConnectDone: func(_, _ string, _ error) {
atomic.CompareAndSwapInt64(&t.ConnEnd, 0, time.Now().UnixNano())
},
TLSHandshakeStart: func() {
atomic.CompareAndSwapInt64(&t.TLSStart, 0, time.Now().UnixNano())
},
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
atomic.CompareAndSwapInt64(&t.TLSEnd, 0, time.Now().UnixNano())
},
GotFirstResponseByte: func() {
atomic.CompareAndSwapInt64(&t.ResponseStart, 0, time.Now().UnixNano())
},
})
return r.WithContext(context.WithValue(ctx, transferKey, t))
}
// LogResponse sends the current response stats to the http log.
//
// DEPRECATED: Use LogRequest() instead.
func (c *Client) LogResponse(key string, res *http.Response) {}
func newSyncLogger(w io.WriteCloser) *syncLogger {
ch := make(chan string, 100)
wg := &sync.WaitGroup{}
wg.Add(1)
go func(c chan string, w io.Writer, wg *sync.WaitGroup) {
for l := range c {
w.Write([]byte(l))
wg.Done()
}
}(ch, w, wg)
return &syncLogger{w: w, ch: ch, wg: wg}
}
type syncLogger struct {
w io.WriteCloser
ch chan string
wg *sync.WaitGroup
}
func (l *syncLogger) LogRequest(req *http.Request, bodySize int64) {
if l == nil {
return
}
if v := req.Context().Value(transferKey); v != nil {
l.logTransfer(v.(*httpTransfer), "request", fmt.Sprintf(" body=%d", bodySize))
}
}
func (l *syncLogger) LogResponse(req *http.Request, status int, bodySize int64) {
if l == nil {
return
}
if v := req.Context().Value(transferKey); v != nil {
t := v.(*httpTransfer)
now := time.Now().UnixNano()
l.logTransfer(t, "response",
fmt.Sprintf(" status=%d body=%d conntime=%d dnstime=%d tlstime=%d restime=%d time=%d",
status,
bodySize,
tools.MaxInt64(t.ConnEnd-t.ConnStart, 0),
tools.MaxInt64(t.DNSEnd-t.DNSStart, 0),
tools.MaxInt64(t.TLSEnd-t.TLSStart, 0),
tools.MaxInt64(now-t.ResponseStart, 0),
tools.MaxInt64(now-t.Start, 0),
))
}
}
func (l *syncLogger) logTransfer(t *httpTransfer, event, extra string) {
l.wg.Add(1)
l.ch <- fmt.Sprintf("key=%s event=%s url=%s method=%s%s\n",
t.Key,
event,
t.URL,
t.Method,
extra,
)
}
func (l *syncLogger) Close() error {
if l == nil {
return nil
}
l.wg.Done()
l.wg.Wait()
return l.w.Close()
}