diff --git a/cli/root.go b/cli/root.go index a71626ccdf3ef..aa61a4eb571fe 100644 --- a/cli/root.go +++ b/cli/root.go @@ -364,6 +364,13 @@ func (r *RootCmd) Command(subcommands []*clibase.Cmd) (*clibase.Cmd, error) { Value: clibase.BoolOf(&r.verbose), Group: globalGroup, }, + { + Flag: "debug-http", + Description: "Debug codersdk HTTP requests.", + Value: clibase.BoolOf(&r.debugHTTP), + Group: globalGroup, + Hidden: true, + }, { Flag: config.FlagName, Env: "CODER_CONFIG_DIR", @@ -412,6 +419,7 @@ type RootCmd struct { forceTTY bool noOpen bool verbose bool + debugHTTP bool noVersionCheck bool noFeatureWarning bool @@ -464,6 +472,11 @@ func (r *RootCmd) InitClient(client *codersdk.Client) clibase.MiddlewareFunc { client.SetSessionToken(r.token) + if r.debugHTTP { + client.PlainLogger = os.Stderr + client.LogBodies = true + } + // We send these requests in parallel to minimize latency. var ( versionErr = make(chan error) diff --git a/codersdk/client.go b/codersdk/client.go index c501de4b574e6..210d5898f1e93 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -10,6 +10,7 @@ import ( "mime" "net" "net/http" + "net/http/httputil" "net/url" "strings" "sync" @@ -90,6 +91,10 @@ type Client struct { // LogBodies can be enabled to print request and response bodies to the logger. LogBodies bool + // PlainLogger may be set to log HTTP traffic in a human-readable form. + // It uses the LogBodies option. + PlainLogger io.Writer + // Trace can be enabled to propagate tracing spans to the Coder API. // This is useful for tracking a request end-to-end. Trace bool @@ -109,6 +114,16 @@ func (c *Client) SetSessionToken(token string) { c.sessionToken = token } +func prefixLines(prefix, s []byte) []byte { + ss := bytes.NewBuffer(make([]byte, 0, len(s)*2)) + for _, line := range bytes.Split(s, []byte("\n")) { + _, _ = ss.Write(prefix) + _, _ = ss.Write(line) + _ = ss.WriteByte('\n') + } + return ss.Bytes() +} + // Request performs a HTTP request with the body provided. The caller is // responsible for closing the response body. func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) { @@ -155,6 +170,15 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return nil, xerrors.Errorf("create request: %w", err) } + if c.PlainLogger != nil { + out, err := httputil.DumpRequest(req, c.LogBodies) + if err != nil { + return nil, xerrors.Errorf("dump request: %w", err) + } + out = prefixLines([]byte("http --> "), out) + _, _ = c.PlainLogger.Write(out) + } + tokenHeader := c.SessionTokenHeader if tokenHeader == "" { tokenHeader = SessionTokenHeader @@ -192,6 +216,15 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac return nil, err } + if c.PlainLogger != nil { + out, err := httputil.DumpResponse(resp, c.LogBodies) + if err != nil { + return nil, xerrors.Errorf("dump response: %w", err) + } + out = prefixLines([]byte("http <-- "), out) + _, _ = c.PlainLogger.Write(out) + } + span.SetAttributes(httpconv.ClientResponse(resp)...) span.SetStatus(httpconv.ClientStatus(resp.StatusCode))