From 2fb6f61a7d550256690e5ff233049efc2adf7578 Mon Sep 17 00:00:00 2001 From: techfg Date: Thu, 26 Jun 2025 13:44:37 -0700 Subject: [PATCH 1/2] fix: do not consume request body unless explicitly requested --- _example/main.go | 28 ++++++++++++++++++++++------ middleware.go | 25 ++++++++++++++++++++++--- options.go | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/_example/main.go b/_example/main.go index 728573f..e15db4d 100644 --- a/_example/main.go +++ b/_example/main.go @@ -78,13 +78,24 @@ func main() { LogResponseBody: isDebugHeaderSet, // Log all requests with invalid payload as curl command. - LogExtraAttrs: func(req *http.Request, reqBody string, respStatus int) []slog.Attr { - if respStatus == 400 || respStatus == 422 { - req.Header.Del("Authorization") - return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))} - } - return nil + LogAdditionalAttrs: &httplog.LogAdditionalAttrsOptions{ + IncludeRequestBody: isAdditionalAttrsDebugHeaderSet, + AdditionalAttrs: func(ld *httplog.LogDetails) []slog.Attr { + if ld.ResponseStatus == 400 || ld.ResponseStatus == 422 { + ld.Request.Header.Del("Authorization") + return []slog.Attr{slog.String("curl", httplog.CURL(ld.Request, ld.RequestBody))} + } + return nil + }, }, + // Deprecated: LogExtraAttrs is deprecated, use LogAdditionalAttrs instead. + // LogExtraAttrs: func(req *http.Request, reqBody string, respStatus int) []slog.Attr { + // if respStatus == 400 || respStatus == 422 { + // req.Header.Del("Authorization") + // return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))} + // } + // return nil + // }, })) // Set request log attribute from within middleware. @@ -190,6 +201,7 @@ func main() { fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"data": "valid payload"}'`) fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"data": "valid payload"}' -H "Debug: reveal-body-logs"`) fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"xx": "invalid payload"}'`) + fmt.Println(` curl -v http://localhost:8000/string/to/upper -X POST --json '{"xx": "invalid payload"}' -H "AdditionalAttrsDebug: reveal-body-logs"`) fmt.Println() if err := http.ListenAndServe("localhost:8000", r); err != http.ErrAbortHandler { @@ -217,3 +229,7 @@ func logHandler(isLocalhost bool, handlerOpts *slog.HandlerOptions) slog.Handler func isDebugHeaderSet(r *http.Request) bool { return r.Header.Get("Debug") == "reveal-body-logs" } + +func isAdditionalAttrsDebugHeaderSet(r *http.Request) bool { + return r.Header.Get("AdditionalAttrsDebug") == "reveal-body-logs" +} diff --git a/middleware.go b/middleware.go index a31f3d8..dbaadfc 100644 --- a/middleware.go +++ b/middleware.go @@ -41,8 +41,19 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand logReqBody := o.LogRequestBody != nil && o.LogRequestBody(r) logRespBody := o.LogResponseBody != nil && o.LogResponseBody(r) + var includeAdditionalAttrsReqBody bool + if o.LogAdditionalAttrs != nil { + if o.LogAdditionalAttrs.AdditionalAttrs != nil && o.LogAdditionalAttrs.IncludeRequestBody != nil { + includeAdditionalAttrsReqBody = o.LogAdditionalAttrs.IncludeRequestBody(r) + } + } else if o.LogExtraAttrs != nil { + includeAdditionalAttrsReqBody = true + } + hasReqBody := r.Body != nil && r.Body != http.NoBody + consumeBody := hasReqBody && (logReqBody || includeAdditionalAttrsReqBody) + var reqBody bytes.Buffer - if logReqBody || o.LogExtraAttrs != nil { + if consumeBody { r.Body = io.NopCloser(io.TeeReader(r.Body, &reqBody)) } @@ -142,7 +153,7 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand logAttrs = appendAttrs(logAttrs, slog.Any(ErrorKey, ErrClientAborted), slog.String(s.ErrorType, "ClientAborted")) } - if logReqBody || o.LogExtraAttrs != nil { + if consumeBody { // Ensure the request body is fully read if the underlying HTTP handler didn't do so. n, _ := io.Copy(io.Discard, r.Body) if n > 0 { @@ -155,7 +166,15 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand if logRespBody { logAttrs = appendAttrs(logAttrs, slog.String(s.ResponseBody, logBody(&respBody, ww.Header(), o))) } - if o.LogExtraAttrs != nil { + if o.LogAdditionalAttrs != nil { + if o.LogAdditionalAttrs.AdditionalAttrs != nil { + logAttrs = appendAttrs(logAttrs, o.LogAdditionalAttrs.AdditionalAttrs(&LogDetails{ + Request: r, + RequestBody: reqBody.String(), + ResponseStatus: statusCode, + })...) + } + } else if o.LogExtraAttrs != nil { logAttrs = appendAttrs(logAttrs, o.LogExtraAttrs(r, reqBody.String(), statusCode)...) } logAttrs = appendAttrs(logAttrs, getAttrs(ctx)...) diff --git a/options.go b/options.go index 6dc7661..cd5b5d8 100644 --- a/options.go +++ b/options.go @@ -94,7 +94,15 @@ type Options struct { // } // // WARNING: Be careful not to leak any sensitive information in the logs. + // + // Deprecated: Use LogAdditionalAttrs instead. Will be ignored if LogAdditionalAttrs is set. LogExtraAttrs func(req *http.Request, reqBody string, respStatus int) []slog.Attr + + // LogAdditionalAttrs is an optional way for you to add additional attributes to the + // request log. + // + // WARNING: Be careful not to leak any sensitive information in the logs. + LogAdditionalAttrs *LogAdditionalAttrsOptions } var defaultOptions = Options{ @@ -106,3 +114,36 @@ var defaultOptions = Options{ LogBodyContentTypes: []string{"application/json", "application/xml", "text/plain", "text/csv", "application/x-www-form-urlencoded", ""}, LogBodyMaxLen: 1024, } + +type LogAdditionalAttrsOptions struct { + // IncludeRequestBody is an optional predicate function that controls if LogDetails.RequestBody passed to AdditionalAttrs will contain the full request body. + // + // If the function returns true or if Options.LogRequestBody returns true, the request body will be passed in LogDetails.RequestBody. + // If false or not set and Options.LogRequestBody is not set or returns false, an empty string will be passed in LogDetails.RequestBody. + // + // WARNING: Do not leak any request bodies with sensitive information. + IncludeRequestBody func(req *http.Request) bool + // AdditionalAttrs is an optional way for you to add additional attributes to the + // request log. + // + // Example: + // + // // Log all requests with invalid payload as curl command. + // func(ld *LogDetails) []slog.Attr { + // if ld.ResponseStatus == 400 || ld.ResponseStatus == 422 { + // ld.Request.Header.Del("Authorization") + // return []slog.Attr{slog.String("curl", httplog.CURL(req, reqBody))} + // } + // return nil + // } + // + // WARNING: Be careful not to leak any sensitive information in the logs. + AdditionalAttrs func(ld *LogDetails) []slog.Attr +} + +type LogDetails struct { + Request *http.Request + // Contains the request body if either Options.LogRequestBody or LogAdditionalAttrsOptions.IncludeRequestBody is true, otherwise it is empty. + RequestBody string + ResponseStatus int +} From b40e9b5f0aef9af344a19e0e9f7dc062f6127b60 Mon Sep 17 00:00:00 2001 From: techfg Date: Thu, 26 Jun 2025 13:51:31 -0700 Subject: [PATCH 2/2] feat: expose byteswritten to AdditionalAttrs --- middleware.go | 15 +++++++++------ options.go | 7 +++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/middleware.go b/middleware.go index dbaadfc..50047a9 100644 --- a/middleware.go +++ b/middleware.go @@ -153,11 +153,12 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand logAttrs = appendAttrs(logAttrs, slog.Any(ErrorKey, ErrClientAborted), slog.String(s.ErrorType, "ClientAborted")) } + var reqUnreadBytes int64 if consumeBody { // Ensure the request body is fully read if the underlying HTTP handler didn't do so. - n, _ := io.Copy(io.Discard, r.Body) - if n > 0 { - logAttrs = appendAttrs(logAttrs, slog.Any(s.RequestBytesUnread, n)) + reqUnreadBytes, _ = io.Copy(io.Discard, r.Body) + if reqUnreadBytes > 0 { + logAttrs = appendAttrs(logAttrs, slog.Int64(s.RequestBytesUnread, reqUnreadBytes)) } } if logReqBody { @@ -169,9 +170,11 @@ func RequestLogger(logger *slog.Logger, o *Options) func(http.Handler) http.Hand if o.LogAdditionalAttrs != nil { if o.LogAdditionalAttrs.AdditionalAttrs != nil { logAttrs = appendAttrs(logAttrs, o.LogAdditionalAttrs.AdditionalAttrs(&LogDetails{ - Request: r, - RequestBody: reqBody.String(), - ResponseStatus: statusCode, + Request: r, + RequestBody: reqBody.String(), + RequestBytesUnread: reqUnreadBytes, + ResponseStatus: statusCode, + ResponseBytes: ww.BytesWritten(), })...) } } else if o.LogExtraAttrs != nil { diff --git a/options.go b/options.go index cd5b5d8..778bbcc 100644 --- a/options.go +++ b/options.go @@ -144,6 +144,9 @@ type LogAdditionalAttrsOptions struct { type LogDetails struct { Request *http.Request // Contains the request body if either Options.LogRequestBody or LogAdditionalAttrsOptions.IncludeRequestBody is true, otherwise it is empty. - RequestBody string - ResponseStatus int + RequestBody string + // Contains the number of unread bytes from the request body if either Options.LogRequestBody or LogAdditionalAttrsOptions.IncludeRequestBody is true, otherwise it is 0. + RequestBytesUnread int64 + ResponseStatus int + ResponseBytes int }