Skip to content

Commit 7a19984

Browse files
committed
Deprecate middleware.Logger
1 parent 8389d2e commit 7a19984

File tree

5 files changed

+192
-4
lines changed

5 files changed

+192
-4
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ var DefaultLoggerConfig = LoggerConfig{
237237
// "bytes_in":0,"bytes_out":42}
238238
//
239239
// For custom configurations, use LoggerWithConfig instead.
240+
//
241+
// Deprecated: please use middleware.RequestLogger or middleware.RequestLoggerWithConfig instead.
240242
func Logger() echo.MiddlewareFunc {
241243
return LoggerWithConfig(DefaultLoggerConfig)
242244
}
@@ -261,6 +263,8 @@ func Logger() echo.MiddlewareFunc {
261263
// return c.Request().URL.Path == "/health"
262264
// },
263265
// }))
266+
//
267+
// Deprecated: please use middleware.RequestLoggerWithConfig instead.
264268
func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
265269
// Defaults
266270
if config.Skipper == nil {

middleware/request_logger.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
package middleware
55

66
import (
7+
"context"
78
"errors"
9+
"log/slog"
810
"net/http"
911
"time"
1012

@@ -247,6 +249,72 @@ func RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc {
247249
return mw
248250
}
249251

252+
// RequestLogger returns a RequestLogger middleware with default configuration which
253+
// uses default slog.slog logger.
254+
//
255+
// To customize slog output format replace slog default logger:
256+
// For JSON format: `slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))`
257+
func RequestLogger() echo.MiddlewareFunc {
258+
config := RequestLoggerConfig{
259+
LogLatency: true,
260+
LogProtocol: false,
261+
LogRemoteIP: true,
262+
LogHost: true,
263+
LogMethod: true,
264+
LogURI: true,
265+
LogURIPath: false,
266+
LogRoutePath: false,
267+
LogRequestID: true,
268+
LogReferer: false,
269+
LogUserAgent: true,
270+
LogStatus: true,
271+
LogError: true,
272+
LogContentLength: true,
273+
LogResponseSize: true,
274+
LogHeaders: nil,
275+
LogQueryParams: nil,
276+
LogFormValues: nil,
277+
HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
278+
LogValuesFunc: func(c echo.Context, v RequestLoggerValues) error {
279+
if v.Error == nil {
280+
slog.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
281+
slog.String("method", v.Method),
282+
slog.String("uri", v.URI),
283+
slog.Int("status", v.Status),
284+
slog.Duration("latency", v.Latency),
285+
slog.String("host", v.Host),
286+
slog.String("bytes_in", v.ContentLength),
287+
slog.Int64("bytes_out", v.ResponseSize),
288+
slog.String("user_agent", v.UserAgent),
289+
slog.String("remote_ip", v.RemoteIP),
290+
slog.String("request_id", v.RequestID),
291+
)
292+
} else {
293+
slog.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
294+
slog.String("method", v.Method),
295+
slog.String("uri", v.URI),
296+
slog.Int("status", v.Status),
297+
slog.Duration("latency", v.Latency),
298+
slog.String("host", v.Host),
299+
slog.String("bytes_in", v.ContentLength),
300+
slog.Int64("bytes_out", v.ResponseSize),
301+
slog.String("user_agent", v.UserAgent),
302+
slog.String("remote_ip", v.RemoteIP),
303+
slog.String("request_id", v.RequestID),
304+
305+
slog.String("error", v.Error.Error()),
306+
)
307+
}
308+
return nil
309+
},
310+
}
311+
mw, err := config.ToMiddleware()
312+
if err != nil {
313+
panic(err)
314+
}
315+
return mw
316+
}
317+
250318
// ToMiddleware converts RequestLoggerConfig into middleware or returns an error for invalid configuration.
251319
func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
252320
if config.Skipper == nil {

middleware/request_logger_test.go

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,116 @@
44
package middleware
55

66
import (
7-
"github.com/labstack/echo/v4"
8-
"github.com/stretchr/testify/assert"
7+
"bytes"
8+
"encoding/json"
9+
"errors"
10+
"log/slog"
911
"net/http"
1012
"net/http/httptest"
1113
"net/url"
1214
"strconv"
1315
"strings"
1416
"testing"
1517
"time"
18+
19+
"github.com/labstack/echo/v4"
20+
"github.com/stretchr/testify/assert"
1621
)
1722

23+
func TestRequestLoggerOK(t *testing.T) {
24+
old := slog.Default()
25+
t.Cleanup(func() {
26+
slog.SetDefault(old)
27+
})
28+
29+
buf := new(bytes.Buffer)
30+
slog.SetDefault(slog.New(slog.NewJSONHandler(buf, nil)))
31+
32+
e := echo.New()
33+
e.Use(RequestLogger())
34+
35+
e.POST("/test", func(c echo.Context) error {
36+
return c.String(http.StatusTeapot, "OK")
37+
})
38+
39+
reader := strings.NewReader(`{"foo":"bar"}`)
40+
req := httptest.NewRequest(http.MethodPost, "/test", reader)
41+
req.Header.Set(echo.HeaderContentLength, strconv.Itoa(int(reader.Size())))
42+
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
43+
req.Header.Set(echo.HeaderXRealIP, "8.8.8.8")
44+
req.Header.Set("User-Agent", "curl/7.68.0")
45+
46+
rec := httptest.NewRecorder()
47+
e.ServeHTTP(rec, req)
48+
49+
logAttrs := map[string]interface{}{}
50+
assert.NoError(t, json.Unmarshal(buf.Bytes(), &logAttrs))
51+
logAttrs["latency"] = 123
52+
logAttrs["time"] = "x"
53+
54+
expect := map[string]interface{}{
55+
"level": "INFO",
56+
"msg": "REQUEST",
57+
"method": "POST",
58+
"uri": "/test",
59+
"status": float64(418),
60+
"bytes_in": "13",
61+
"host": "example.com",
62+
"bytes_out": float64(2),
63+
"user_agent": "curl/7.68.0",
64+
"remote_ip": "8.8.8.8",
65+
"request_id": "",
66+
67+
"time": "x",
68+
"latency": 123,
69+
}
70+
assert.Equal(t, expect, logAttrs)
71+
}
72+
73+
func TestRequestLoggerError(t *testing.T) {
74+
old := slog.Default()
75+
t.Cleanup(func() {
76+
slog.SetDefault(old)
77+
})
78+
79+
buf := new(bytes.Buffer)
80+
slog.SetDefault(slog.New(slog.NewJSONHandler(buf, nil)))
81+
82+
e := echo.New()
83+
e.Use(RequestLogger())
84+
85+
e.GET("/test", func(c echo.Context) error {
86+
return errors.New("nope")
87+
})
88+
req := httptest.NewRequest(http.MethodGet, "/test", nil)
89+
rec := httptest.NewRecorder()
90+
e.ServeHTTP(rec, req)
91+
92+
logAttrs := map[string]interface{}{}
93+
assert.NoError(t, json.Unmarshal(buf.Bytes(), &logAttrs))
94+
logAttrs["latency"] = 123
95+
logAttrs["time"] = "x"
96+
97+
expect := map[string]interface{}{
98+
"level": "ERROR",
99+
"msg": "REQUEST_ERROR",
100+
"method": "GET",
101+
"uri": "/test",
102+
"status": float64(500),
103+
"bytes_in": "",
104+
"host": "example.com",
105+
"bytes_out": float64(36.0),
106+
"user_agent": "",
107+
"remote_ip": "192.0.2.1",
108+
"request_id": "",
109+
"error": "nope",
110+
111+
"latency": 123,
112+
"time": "x",
113+
}
114+
assert.Equal(t, expect, logAttrs)
115+
}
116+
18117
func TestRequestLoggerWithConfig(t *testing.T) {
19118
e := echo.New()
20119

0 commit comments

Comments
 (0)