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)