diff --git a/client.go b/client.go index 1bf4a154d..27b764def 100644 --- a/client.go +++ b/client.go @@ -13,6 +13,7 @@ import ( "os" "reflect" "sort" + "strings" "sync" "time" @@ -212,10 +213,26 @@ func NewClient(options ClientOptions) (*Client, error) { options.Environment = os.Getenv("SENTRY_ENVIRONMENT") } - if env := os.Getenv("SENTRYGODEBUG"); env == "dumphttp=1" { + // SENTRYGODEBUG is a comma-separated list of key=value pairs (similar + // to GODEBUG). It is not a supported feature: recognized debug options + // may change any time. + // + // The intended public is SDK developers. It is orthogonal to + // options.Debug, which is also available for SDK users. + dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",") + sort.Strings(dbg) + // dbgOpt returns true when the given debug option is enabled, for + // example SENTRYGODEBUG=someopt=1. + dbgOpt := func(opt string) bool { + s := opt + "=1" + return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s + } + if dbgOpt("httpdump") || dbgOpt("httptrace") { options.HTTPTransport = &debug.Transport{ RoundTripper: http.DefaultTransport, Output: os.Stderr, + Dump: dbgOpt("httpdump"), + Trace: dbgOpt("httptrace"), } } diff --git a/internal/debug/transport.go b/internal/debug/transport.go index fe5ff291d..199c3a303 100644 --- a/internal/debug/transport.go +++ b/internal/debug/transport.go @@ -1,38 +1,72 @@ package debug import ( + "bytes" + "fmt" "io" "net/http" + "net/http/httptrace" "net/http/httputil" ) // Transport implements http.RoundTripper and can be used to wrap other HTTP -// transports to dump request and responses for debugging. +// transports for debugging, normally http.DefaultTransport. type Transport struct { http.RoundTripper Output io.Writer + // Dump controls whether to dump HTTP request and responses. + Dump bool + // Trace enables usage of net/http/httptrace. + Trace bool } func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { - b, err := httputil.DumpRequestOut(req, true) - if err != nil { - return nil, err + var buf bytes.Buffer + if t.Dump { + b, err := httputil.DumpRequestOut(req, true) + if err != nil { + panic(err) + } + _, err = buf.Write(ensureTrailingNewline(b)) + if err != nil { + panic(err) + } } - _, err = t.Output.Write(ensureTrailingNewline(b)) - if err != nil { - return nil, err + if t.Trace { + trace := &httptrace.ClientTrace{ + DNSDone: func(di httptrace.DNSDoneInfo) { + fmt.Fprintf(&buf, "* DNS %v → %v\n", req.Host, di.Addrs) + }, + GotConn: func(ci httptrace.GotConnInfo) { + fmt.Fprintf(&buf, "* Connection local=%v remote=%v", ci.Conn.LocalAddr(), ci.Conn.RemoteAddr()) + if ci.Reused { + fmt.Fprint(&buf, " (reused)") + } + if ci.WasIdle { + fmt.Fprintf(&buf, " (idle %v)", ci.IdleTime) + } + fmt.Fprintln(&buf) + }, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) } resp, err := t.RoundTripper.RoundTrip(req) if err != nil { return nil, err } - b, err = httputil.DumpResponse(resp, true) - if err != nil { - return nil, err + if t.Dump { + b, err := httputil.DumpResponse(resp, true) + if err != nil { + panic(err) + } + _, err = buf.Write(ensureTrailingNewline(b)) + if err != nil { + panic(err) + } } - _, err = t.Output.Write(ensureTrailingNewline(b)) + _, err = io.Copy(t.Output, &buf) if err != nil { - return nil, err + panic(err) } return resp, nil }