Skip to content

Commit c12cb08

Browse files
authored
Logger middleware json string escaping and deprecation (#2849)
* Logger middleware should escape string values when outputting JSON * Add Go license to logger_strings.go * Deprecate middleware.Logger
1 parent 612967a commit c12cb08

File tree

8 files changed

+1066
-136
lines changed

8 files changed

+1066
-136
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Changelog
22

3+
## v4.14.0 - 2025-12-xx
4+
5+
**Security**
6+
7+
* Logger middleware: escape string values when logger format looks like JSON
8+
9+
10+
**Enhancements**
11+
12+
* Add `middleware.RequestLogger` function to replace `middleware.Logger`. `middleware.RequestLogger` uses default slog logger.
13+
Default slog logger output can be configured to JSON format like that:
14+
```go
15+
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
16+
e.Use(middleware.RequestLogger())
17+
```
18+
* Deprecate `middleware.Logger` function and point users to `middleware.RequestLogger` and `middleware.RequestLoggerWithConfig`
19+
320
## v4.13.4 - 2025-05-22
421

522
**Enhancements**

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ func main() {
7373
e := echo.New()
7474

7575
// Middleware
76-
e.Use(middleware.Logger())
77-
e.Use(middleware.Recover())
76+
e.Use(middleware.RequestLogger()) // use the default RequestLogger middleware with slog logger
77+
e.Use(middleware.Recover()) // recover panics as errors for proper error handling
7878

7979
// Routes
8080
e.GET("/", hello)

middleware/logger.go

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ type LoggerConfig struct {
197197
template *fasttemplate.Template
198198
colorer *color.Color
199199
pool *sync.Pool
200+
timeNow func() time.Time
200201
}
201202

202203
// DefaultLoggerConfig is the default Logger middleware config.
@@ -208,6 +209,7 @@ var DefaultLoggerConfig = LoggerConfig{
208209
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
209210
CustomTimeFormat: "2006-01-02 15:04:05.00000",
210211
colorer: color.New(),
212+
timeNow: time.Now,
211213
}
212214

213215
// Logger returns a middleware that logs HTTP requests using the default configuration.
@@ -235,6 +237,8 @@ var DefaultLoggerConfig = LoggerConfig{
235237
// "bytes_in":0,"bytes_out":42}
236238
//
237239
// For custom configurations, use LoggerWithConfig instead.
240+
//
241+
// Deprecated: please use middleware.RequestLogger or middleware.RequestLoggerWithConfig instead.
238242
func Logger() echo.MiddlewareFunc {
239243
return LoggerWithConfig(DefaultLoggerConfig)
240244
}
@@ -259,6 +263,8 @@ func Logger() echo.MiddlewareFunc {
259263
// return c.Request().URL.Path == "/health"
260264
// },
261265
// }))
266+
//
267+
// Deprecated: please use middleware.RequestLoggerWithConfig instead.
262268
func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
263269
// Defaults
264270
if config.Skipper == nil {
@@ -267,9 +273,18 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
267273
if config.Format == "" {
268274
config.Format = DefaultLoggerConfig.Format
269275
}
276+
writeString := func(buf *bytes.Buffer, in string) (int, error) { return buf.WriteString(in) }
277+
if config.Format[0] == '{' { // format looks like JSON, so we need to escape invalid characters
278+
writeString = writeJSONSafeString
279+
}
280+
270281
if config.Output == nil {
271282
config.Output = DefaultLoggerConfig.Output
272283
}
284+
timeNow := DefaultLoggerConfig.timeNow
285+
if config.timeNow != nil {
286+
timeNow = config.timeNow
287+
}
273288

274289
config.template = fasttemplate.New(config.Format, "${", "}")
275290
config.colorer = color.New()
@@ -305,49 +320,47 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
305320
}
306321
return config.CustomTagFunc(c, buf)
307322
case "time_unix":
308-
return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
323+
return buf.WriteString(strconv.FormatInt(timeNow().Unix(), 10))
309324
case "time_unix_milli":
310-
// go 1.17 or later, it supports time#UnixMilli()
311-
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000000, 10))
325+
return buf.WriteString(strconv.FormatInt(timeNow().UnixMilli(), 10))
312326
case "time_unix_micro":
313-
// go 1.17 or later, it supports time#UnixMicro()
314-
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000, 10))
327+
return buf.WriteString(strconv.FormatInt(timeNow().UnixMicro(), 10))
315328
case "time_unix_nano":
316-
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10))
329+
return buf.WriteString(strconv.FormatInt(timeNow().UnixNano(), 10))
317330
case "time_rfc3339":
318-
return buf.WriteString(time.Now().Format(time.RFC3339))
331+
return buf.WriteString(timeNow().Format(time.RFC3339))
319332
case "time_rfc3339_nano":
320-
return buf.WriteString(time.Now().Format(time.RFC3339Nano))
333+
return buf.WriteString(timeNow().Format(time.RFC3339Nano))
321334
case "time_custom":
322-
return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
335+
return buf.WriteString(timeNow().Format(config.CustomTimeFormat))
323336
case "id":
324337
id := req.Header.Get(echo.HeaderXRequestID)
325338
if id == "" {
326339
id = res.Header().Get(echo.HeaderXRequestID)
327340
}
328-
return buf.WriteString(id)
341+
return writeString(buf, id)
329342
case "remote_ip":
330-
return buf.WriteString(c.RealIP())
343+
return writeString(buf, c.RealIP())
331344
case "host":
332-
return buf.WriteString(req.Host)
345+
return writeString(buf, req.Host)
333346
case "uri":
334-
return buf.WriteString(req.RequestURI)
347+
return writeString(buf, req.RequestURI)
335348
case "method":
336-
return buf.WriteString(req.Method)
349+
return writeString(buf, req.Method)
337350
case "path":
338351
p := req.URL.Path
339352
if p == "" {
340353
p = "/"
341354
}
342-
return buf.WriteString(p)
355+
return writeString(buf, p)
343356
case "route":
344-
return buf.WriteString(c.Path())
357+
return writeString(buf, c.Path())
345358
case "protocol":
346-
return buf.WriteString(req.Proto)
359+
return writeString(buf, req.Proto)
347360
case "referer":
348-
return buf.WriteString(req.Referer())
361+
return writeString(buf, req.Referer())
349362
case "user_agent":
350-
return buf.WriteString(req.UserAgent())
363+
return writeString(buf, req.UserAgent())
351364
case "status":
352365
n := res.Status
353366
s := config.colorer.Green(n)
@@ -377,17 +390,17 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
377390
if cl == "" {
378391
cl = "0"
379392
}
380-
return buf.WriteString(cl)
393+
return writeString(buf, cl)
381394
case "bytes_out":
382395
return buf.WriteString(strconv.FormatInt(res.Size, 10))
383396
default:
384397
switch {
385398
case strings.HasPrefix(tag, "header:"):
386-
return buf.Write([]byte(c.Request().Header.Get(tag[7:])))
399+
return writeString(buf, c.Request().Header.Get(tag[7:]))
387400
case strings.HasPrefix(tag, "query:"):
388-
return buf.Write([]byte(c.QueryParam(tag[6:])))
401+
return writeString(buf, c.QueryParam(tag[6:]))
389402
case strings.HasPrefix(tag, "form:"):
390-
return buf.Write([]byte(c.FormValue(tag[5:])))
403+
return writeString(buf, c.FormValue(tag[5:]))
391404
case strings.HasPrefix(tag, "cookie:"):
392405
cookie, err := c.Cookie(tag[7:])
393406
if err == nil {

0 commit comments

Comments
 (0)