Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logging level middleware #2268

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions middleware/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type (
// - referer
// - user_agent
// - status
// - level
// - error
// - latency (In nanoseconds)
// - latency_human (Human readable)
Expand All @@ -60,6 +61,10 @@ type (
// Optional. Default value os.Stdout.
Output io.Writer

// LevelSetter is a logging level setting function.
// Optional. Default value is DefaultLoggingLevelSetter.
LevelSetter LoggingLevelSetterFunc

template *fasttemplate.Template
colorer *color.Color
pool *sync.Pool
Expand All @@ -75,6 +80,7 @@ var (
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000",
LevelSetter: DefaultLoggingLevelSetter,
colorer: color.New(),
}
)
Expand All @@ -97,6 +103,9 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
if config.Output == nil {
config.Output = DefaultLoggerConfig.Output
}
if config.LevelSetter == nil {
config.LevelSetter = DefaultLoggerConfig.LevelSetter
}

config.template = fasttemplate.New(config.Format, "${", "}")
config.colorer = color.New()
Expand Down Expand Up @@ -180,6 +189,9 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
s = config.colorer.Cyan(n)
}
return buf.WriteString(s)
case "level":
level := config.LevelSetter(c, err)
return buf.WriteString(level)
case "error":
if err != nil {
// Error may contain invalid JSON e.g. `"`
Expand Down
150 changes: 149 additions & 1 deletion middleware/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func TestLoggerTemplate(t *testing.T) {
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in}, "path":"${path}", "referer":"${referer}",` +
`"bytes_out":${bytes_out},"ch":"${header:X-Custom-Header}", "protocol":"${protocol}"` +
`"us":"${query:username}", "cf":"${form:username}", "session":"${cookie:session}"}` + "\n",
`"us":"${query:username}", "cf":"${form:username}", "session":"${cookie:session}"}, "level":"${level}"}` + "\n",
Output: buf,
}))

Expand Down Expand Up @@ -135,6 +135,7 @@ func TestLoggerTemplate(t *testing.T) {
"echo-tests-agent": true,
"6ba7b810-9dad-11d1-80b4-00c04fd430c8": true,
"ac08034cd216a647fc2eb62f2bcf7b810": true,
"info": true,
}

for token, present := range cases {
Expand Down Expand Up @@ -291,3 +292,150 @@ func TestLoggerTemplateWithTimeUnixMicro(t *testing.T) {
assert.NoError(t, err)
assert.WithinDuration(t, time.Unix(unixMicros/1000000, 0), time.Now(), 3*time.Second)
}

func TestLoggerTemplateWithLevel_withNoError(t *testing.T) {
buf := new(bytes.Buffer)

e := echo.New()
e.Use(LoggerWithConfig(LoggerConfig{
Format: `${level}`,
Output: buf,
LevelSetter: func(_ echo.Context, err error) string {
if err != nil {
return "foo"
}
return "bar"
},
}))

e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "OK")
})

req := httptest.NewRequest(http.MethodGet, "/", nil)

rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, "bar", buf.String())
}

func TestLoggerTemplateWithLevel_withError(t *testing.T) {
buf := new(bytes.Buffer)

e := echo.New()
e.Use(LoggerWithConfig(LoggerConfig{
Format: `${level}`,
Output: buf,
LevelSetter: func(_ echo.Context, err error) string {
if err != nil {
return "foo"
}
return "bar"
},
}))

e.GET("/", func(c echo.Context) error {
return errors.New("error")
})

req := httptest.NewRequest(http.MethodGet, "/", nil)

rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, "foo", buf.String())
}

func TestLoggerTemplateWithLevel_withStatusCodeOK(t *testing.T) {
buf := new(bytes.Buffer)

e := echo.New()
e.Use(LoggerWithConfig(LoggerConfig{
Format: `${level}`,
Output: buf,
LevelSetter: func(c echo.Context, err error) string {
res := c.Response().Status
switch {
case res >= 500:
return "error"
case res >= 400:
return "warn"
default:
return "info"
}
},
}))

e.GET("/", func(c echo.Context) error {
return c.String(200, "foo")
})

req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, "info", buf.String())
}

func TestLoggerTemplateWithLevel_withStatusCodeBadRequest(t *testing.T) {
buf := new(bytes.Buffer)

e := echo.New()
e.Use(LoggerWithConfig(LoggerConfig{
Format: `${level}`,
Output: buf,
LevelSetter: func(c echo.Context, err error) string {
res := c.Response().Status
switch {
case res >= 500:
return "error"
case res >= 400:
return "warn"
default:
return "info"
}
},
}))

e.GET("/", func(c echo.Context) error {
return c.String(400, "foo")
})

req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, "warn", buf.String())
}

func TestLoggerTemplateWithLevel_withStatusCodeInternalServerError(t *testing.T) {
buf := new(bytes.Buffer)

e := echo.New()
e.Use(LoggerWithConfig(LoggerConfig{
Format: `${level}`,
Output: buf,
LevelSetter: func(c echo.Context, err error) string {
res := c.Response().Status
switch {
case res >= 500:
return "error"
case res >= 400:
return "warn"
default:
return "info"
}
},
}))

e.GET("/", func(c echo.Context) error {
return c.String(500, "foo")
})

req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, "error", buf.String())
}
11 changes: 11 additions & 0 deletions middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type (

// BeforeFunc defines a function which is executed just before the middleware.
BeforeFunc func(c echo.Context)

// LoggingLevelSetterFunc allow set login level based on context and error.
LoggingLevelSetterFunc func(c echo.Context, err error) string
)

func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
Expand Down Expand Up @@ -87,3 +90,11 @@ func rewriteURL(rewriteRegex map[*regexp.Regexp]string, req *http.Request) error
func DefaultSkipper(echo.Context) bool {
return false
}

// DefaultLoggingLevelSetter returns "error" when error is not nil and returns info when error is nil.
func DefaultLoggingLevelSetter(_ echo.Context, err error) string {
if err != nil {
return "error"
}
return "info"
}
14 changes: 13 additions & 1 deletion middleware/middleware_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package middleware

import (
"github.com/stretchr/testify/assert"
"errors"
"net/http"
"net/http/httptest"
"regexp"
"testing"

"github.com/stretchr/testify/assert"
)

func TestRewriteURL(t *testing.T) {
Expand Down Expand Up @@ -90,3 +92,13 @@ func TestRewriteURL(t *testing.T) {
})
}
}

func TestDefaultLoggingLevelSetter_withError(t *testing.T) {
level := DefaultLoggingLevelSetter(nil, errors.New("error"))
assert.Equal(t, "error", level)
}

func TestDefaultLoggingLevelSetter_withoutError(t *testing.T) {
level := DefaultLoggingLevelSetter(nil, nil)
assert.Equal(t, "info", level)
}