diff --git a/mittwaldv2/client_opt_logging.go b/mittwaldv2/client_opt_logging.go new file mode 100644 index 00000000..d9ea8acb --- /dev/null +++ b/mittwaldv2/client_opt_logging.go @@ -0,0 +1,21 @@ +package mittwaldv2 + +import ( + "context" + "log/slog" + + "github.com/mittwald/api-client-go/pkg/httpclient" +) + +// WithRequestLogging adds a logging middleware to the request runner chain +// allowing you to log all executed HTTP requests in a slog.Logger of your +// choice. +// +// Be mindful of the log{Request,Response}Bodies parameters; these will cause +// the logger to print the full request bodies without redaction, which may +// easily leak sensitive data. +func WithRequestLogging(logger *slog.Logger, logRequestBodies, logResponseBodies bool) ClientOption { + return func(ctx context.Context, runner httpclient.RequestRunner) (httpclient.RequestRunner, error) { + return httpclient.NewLoggingClient(runner, logger, logRequestBodies, logResponseBodies), nil + } +} diff --git a/pkg/httpclient/client_logging.go b/pkg/httpclient/client_logging.go new file mode 100644 index 00000000..342744d9 --- /dev/null +++ b/pkg/httpclient/client_logging.go @@ -0,0 +1,59 @@ +package httpclient + +import ( + "bytes" + "io" + "log/slog" + "net/http" +) + +type loggingClient struct { + inner RequestRunner + logger *slog.Logger + logRequestBodies bool + logResponseBodies bool +} + +func NewLoggingClient(inner RequestRunner, logger *slog.Logger, logRequestBodies, logResponseBodies bool) RequestRunner { + return &loggingClient{ + inner: inner, + logger: logger, + logRequestBodies: logRequestBodies, + logResponseBodies: logResponseBodies, + } +} + +func (c *loggingClient) Do(request *http.Request) (*http.Response, error) { + l := c.logger.With("req.method", request.Method, "req.url", request.URL.String()) + + if c.logRequestBodies && request.Body != nil { + body, err := io.ReadAll(request.Body) + if err != nil { + return nil, err + } + + request.Body = io.NopCloser(bytes.NewBuffer(body)) + l = l.With("req.body", string(body)) + } + + l.Debug("executing request") + + response, err := c.inner.Do(request) + + if response != nil { + l = l.With("res.status", response.StatusCode) + if c.logResponseBodies && response.Body != nil { + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + response.Body = io.NopCloser(bytes.NewBuffer(body)) + l = l.With("res.body", string(body)) + } + } + + l.Debug("received response") + + return response, err +}