diff --git a/hcloud/client.go b/hcloud/client.go index 58dae0e3..24108d78 100644 --- a/hcloud/client.go +++ b/hcloud/client.go @@ -93,7 +93,7 @@ type Client struct { applicationName string applicationVersion string userAgent string - debugWriter io.Writer + debugOpts *DebugOpts instrumentationRegistry prometheus.Registerer handler handler @@ -216,7 +216,28 @@ func WithApplication(name, version string) ClientOption { // writer. To, for example, print debug information on stderr, set it to os.Stderr. func WithDebugWriter(debugWriter io.Writer) ClientOption { return func(client *Client) { - client.debugWriter = debugWriter + client.debugOpts = defaultDebugOpts(debugWriter) + } +} + +// DebugOpts defines the options used by [WithDebugOpts]. +type DebugOpts struct { + WriteRequest func(id string, dump []byte) + WriteResponse func(id string, dump []byte) +} + +func defaultDebugOpts(writer io.Writer) *DebugOpts { + return &DebugOpts{ + WriteRequest: defaultDebugWriter(writer, "Request"), + WriteResponse: defaultDebugWriter(writer, "Response"), + } +} + +// WithDebugOpts configures a Client to print debug information using the given write +// functions. +func WithDebugOpts(opts DebugOpts) ClientOption { + return func(client *Client) { + client.debugOpts = &opts } } diff --git a/hcloud/client_handler.go b/hcloud/client_handler.go index 29a9376d..8a606132 100644 --- a/hcloud/client_handler.go +++ b/hcloud/client_handler.go @@ -21,9 +21,9 @@ func assembleHandlerChain(client *Client) handler { // Start down the chain: sending the http request h := newHTTPHandler(client.httpClient) - // Insert debug writer if enabled - if client.debugWriter != nil { - h = wrapDebugHandler(h, client.debugWriter) + // Insert debug opts if enabled + if client.debugOpts != nil { + h = wrapDebugHandler(h, client.debugOpts) } // Read rate limit headers diff --git a/hcloud/client_handler_debug.go b/hcloud/client_handler_debug.go index 4aa867db..4ffbe1e9 100644 --- a/hcloud/client_handler_debug.go +++ b/hcloud/client_handler_debug.go @@ -1,23 +1,38 @@ package hcloud import ( + "bytes" "context" + "crypto/rand" + "encoding/hex" "fmt" "io" "net/http" "net/http/httputil" ) -func wrapDebugHandler(wrapped handler, output io.Writer) handler { - return &debugHandler{wrapped, output} +type debugWriteFunc func(id string, dump []byte) + +func wrapDebugHandler( + wrapped handler, + opts *DebugOpts, +) handler { + return &debugHandler{ + handler: wrapped, + writeRequest: opts.WriteRequest, + writeResponse: opts.WriteResponse, + } } type debugHandler struct { - handler handler - output io.Writer + handler handler + writeRequest debugWriteFunc + writeResponse debugWriteFunc } func (h *debugHandler) Do(req *http.Request, v any) (resp *Response, err error) { + id := generateRandomID() + // Clone the request, so we can redact the auth header, read the body // and use a new context. cloned, err := cloneRequest(req, context.Background()) @@ -32,7 +47,7 @@ func (h *debugHandler) Do(req *http.Request, v any) (resp *Response, err error) return nil, err } - fmt.Fprintf(h.output, "--- Request:\n%s\n\n", dumpReq) + h.writeRequest(id, dumpReq) resp, err = h.handler.Do(req, v) if err != nil { @@ -44,7 +59,26 @@ func (h *debugHandler) Do(req *http.Request, v any) (resp *Response, err error) return nil, err } - fmt.Fprintf(h.output, "--- Response:\n%s\n\n", dumpResp) + h.writeResponse(id, dumpResp) return resp, err } + +func generateRandomID() string { + b := make([]byte, 4) + _, err := rand.Read(b) + if err != nil { + panic(fmt.Errorf("failed to generate random string: %w", err)) + } + return hex.EncodeToString(b) +} + +func defaultDebugWriter(output io.Writer, title string) func(id string, dump []byte) { + return func(_ string, dump []byte) { + fmt.Fprintf(output, + "--- %s:\n%s\n\n", + title, + bytes.Trim(dump, "\n"), + ) + } +} diff --git a/hcloud/client_handler_debug_test.go b/hcloud/client_handler_debug_test.go index a527053c..2f91cb38 100644 --- a/hcloud/client_handler_debug_test.go +++ b/hcloud/client_handler_debug_test.go @@ -31,7 +31,6 @@ Authorization: REDACTED Accept-Encoding: gzip - `, }, { @@ -47,13 +46,11 @@ Authorization: REDACTED Accept-Encoding: gzip - --- Response: HTTP/1.1 503 Service Unavailable Connection: close - `, }, { @@ -69,7 +66,6 @@ Authorization: REDACTED Accept-Encoding: gzip - --- Response: HTTP/1.1 200 OK Connection: close @@ -85,7 +81,7 @@ Content-Type: application/json buf := bytes.NewBuffer(nil) m := &mockHandler{testCase.wrapped} - h := wrapDebugHandler(m, buf) + h := wrapDebugHandler(m, defaultDebugOpts(buf)) client := NewClient(WithToken("dummy")) client.userAgent = "hcloud-go/testing" diff --git a/hcloud/client_test.go b/hcloud/client_test.go index 6b44f484..95e9651a 100644 --- a/hcloud/client_test.go +++ b/hcloud/client_test.go @@ -309,7 +309,7 @@ func TestClientDoPost(t *testing.T) { debugLog := new(bytes.Buffer) - env.Client.debugWriter = debugLog + env.Client.debugOpts = defaultDebugOpts(debugLog) callCount := 0 env.Mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization")