@marcel91200 / caddy Mirror · read-only / commits / decc8a4d
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 17:15 UTC

logging: `log_append` Early option, Supports `{http.response.body}` (#7368)

* logging: `log_append` early option * logging: `log_append` supports `{http.response.body}` * Convenience auto-early for request body

Francis Lavoie committed Dec 16, 2025 at 23:42 UTC decc8a4d6fc476b21165400ddc3c02d94ee9f47f
2 files changed +91 -6
modules/caddyhttp/logging/caddyfile.go
+7 -1
@@ -15,6 +15,8 @@
15 package logging
16
17 import (
18 + "strings"
19 +
20 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
21 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
22 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
@@ -26,7 +28,7 @@ func init() {
28
29 // parseCaddyfile sets up the log_append handler from Caddyfile tokens. Syntax:
30 //
29 -// log_append [<matcher>] <key> <value>
31 +// log_append [<matcher>] [<]<key> <value>
32 func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
33 handler := new(LogAppend)
34 err := handler.UnmarshalCaddyfile(h.Dispenser)
@@ -43,6 +45,10 @@ func (h *LogAppend) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
45 if !d.NextArg() {
46 return d.ArgErr()
47 }
48 + if strings.HasPrefix(h.Key, "<") && len(h.Key) > 1 {
49 + h.Early = true
50 + h.Key = h.Key[1:]
51 + }
52 h.Value = d.Val()
53 return nil
54 }
modules/caddyhttp/logging/logappend.go renamed
+84 -5
@@ -15,6 +15,8 @@
15 package logging
16
17 import (
18 + "bytes"
19 + "encoding/base64"
20 "net/http"
21 "strings"
22
@@ -42,6 +44,12 @@ type LogAppend struct {
44 // map, the value of that key will be used. Otherwise
45 // the value will be used as-is as a constant string.
46 Value string `json:"value,omitempty"`
47 +
48 + // Early, if true, adds the log field before calling
49 + // the next handler in the chain. By default, the log
50 + // field is added on the way back up the middleware chain,
51 + // after all subsequent handlers have completed.
52 + Early bool `json:"early,omitempty"`
53 }
54
55 // CaddyModule returns the Caddy module information.
@@ -53,13 +61,63 @@ func (LogAppend) CaddyModule() caddy.ModuleInfo {
61 }
62
63 func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
56 - // Run the next handler in the chain first.
64 + // Determine if we need to add the log field early.
65 + // We do if the Early flag is set, or for convenience,
66 + // if the value is a special placeholder for the request body.
67 + needsEarly := h.Early || h.Value == placeholderRequestBody || h.Value == placeholderRequestBodyBase64
68 +
69 + // Check if we need to buffer the response for special placeholders
70 + needsResponseBody := h.Value == placeholderResponseBody || h.Value == placeholderResponseBodyBase64
71 +
72 + if needsEarly && !needsResponseBody {
73 + // Add the log field before calling the next handler
74 + // (but not if we need the response body, which isn't available yet)
75 + h.addLogField(r, nil)
76 + }
77 +
78 + var rec caddyhttp.ResponseRecorder
79 + var buf *bytes.Buffer
80 +
81 + if needsResponseBody {
82 + // Wrap the response writer with a recorder to capture the response body
83 + buf = new(bytes.Buffer)
84 + rec = caddyhttp.NewResponseRecorder(w, buf, func(status int, header http.Header) bool {
85 + // Always buffer the response when we need to log the body
86 + return true
87 + })
88 + w = rec
89 + }
90 +
91 + // Run the next handler in the chain.
92 // If an error occurs, we still want to add
93 // any extra log fields that we can, so we
94 // hold onto the error and return it later.
95 handlerErr := next.ServeHTTP(w, r)
96
62 - // On the way back up the chain, add the extra log field
97 + if needsResponseBody {
98 + // Write the buffered response to the client
99 + if rec.Buffered() {
100 + h.addLogField(r, buf)
101 + err := rec.WriteResponse()
102 + if err != nil {
103 + return err
104 + }
105 + }
106 + return handlerErr
107 + }
108 +
109 + if !h.Early {
110 + // Add the log field after the handler completes
111 + h.addLogField(r, buf)
112 + }
113 +
114 + return handlerErr
115 +}
116 +
117 +// addLogField adds the log field to the request's extra log fields.
118 +// If buf is not nil, it contains the buffered response body for special
119 +// response body placeholders.
120 +func (h LogAppend) addLogField(r *http.Request, buf *bytes.Buffer) {
121 ctx := r.Context()
122
123 vars := ctx.Value(caddyhttp.VarsCtxKey).(map[string]any)
@@ -67,7 +125,21 @@ func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
125 extra := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields)
126
127 var varValue any
70 - if strings.HasPrefix(h.Value, "{") &&
128 +
129 + // Handle special case placeholders for response body
130 + if h.Value == placeholderResponseBody {
131 + if buf != nil {
132 + varValue = buf.String()
133 + } else {
134 + varValue = ""
135 + }
136 + } else if h.Value == placeholderResponseBodyBase64 {
137 + if buf != nil {
138 + varValue = base64.StdEncoding.EncodeToString(buf.Bytes())
139 + } else {
140 + varValue = ""
141 + }
142 + } else if strings.HasPrefix(h.Value, "{") &&
143 strings.HasSuffix(h.Value, "}") &&
144 strings.Count(h.Value, "{") == 1 {
145 // the value looks like a placeholder, so get its value
@@ -84,10 +156,17 @@ func (h LogAppend) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
156 // We use zap.Any because it will reflect
157 // to the correct type for us.
158 extra.Add(zap.Any(h.Key, varValue))
87 -
88 - return handlerErr
159 }
160
161 +const (
162 + // Special placeholder values that are handled by log_append
163 + // rather than by the replacer.
164 + placeholderRequestBody = "{http.request.body}"
165 + placeholderRequestBodyBase64 = "{http.request.body_base64}"
166 + placeholderResponseBody = "{http.response.body}"
167 + placeholderResponseBodyBase64 = "{http.response.body_base64}"
168 +)
169 +
170 // Interface guards
171 var (
172 _ caddyhttp.MiddlewareHandler = (*LogAppend)(nil)