Skip to content
Merged
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
22 changes: 4 additions & 18 deletions internal/httpserver/middleware/access_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ import (
// accessLogOnceCtxKey is a context key type for the inject log middleware to prevent duplicate injection.
type accessLogOnceCtxKey struct{}

// NewAccessLog returns a middleware that logs HTTP requests at the specified logger.Level (http errors and server
// errors are automatically logged at higher levels). The log entry will include the request URI, method, user agent,
// host, remote address and other relevant information.
// NewAccessLog returns a middleware that logs HTTP requests at the specified logger.Level. The log entry will include
// the request URI, method, user agent, host, remote address and other relevant information.
//
// It depends on the logger being injected into the request context by the injectLog middleware, so it should be used
// AFTER the NewInjectLog middleware in the middleware chain.
Expand All @@ -34,7 +33,7 @@ type accessLogOnceCtxKey struct{}
// This middleware includes a guard to prevent duplicate logging for the same request, in case it is
// accidentally added multiple times in the middleware chain.
func NewAccessLog(
lvl logger.Level, // zapcore.InfoLevel by default, because it's zero-value of the zapcore.Level
lvl logger.Level, // logger.InfoLevel by default, because it's the zero value of logger.Level
skipper func(*http.Request) bool, // optional, may be nil
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
Expand All @@ -60,24 +59,11 @@ func NewAccessLog(
rw := &accessLogResponseWriter{orig: w}

defer func() {
message, level := "Request successfully processed", lvl

status := int(rw.Status.Load())
if status == 0 {
status = http.StatusOK
}

switch {
case status >= http.StatusInternalServerError: // 500
message, level = "Server error", logger.ErrorLevel
case status >= http.StatusBadRequest: // 400
message, level = "Client error", logger.WarnLevel
case status >= http.StatusMultipleChoices: // 300
message = "Redirection"
case status >= http.StatusContinue && status < http.StatusOK: // 1xx
message = "Informational"
}

attrs := []logger.Attr{
logger.Duration("duration", time.Since(now).Round(time.Microsecond)),
logger.Int("status_code", status),
Expand All @@ -101,7 +87,7 @@ func NewAccessLog(
)
}

log.Log(level, message, attrs...)
log.Log(lvl, "Request successfully processed", attrs...)
}()

next.ServeHTTP(rw, r)
Expand Down
16 changes: 8 additions & 8 deletions internal/httpserver/middleware/access_log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,39 +52,39 @@ func TestNewAccessLog(t *testing.T) {
assert.True(t, e.Attrs["duration"].Duration() >= 0)
},
},
"3xx: logged at configured level with redirection message": {
"3xx: logged at configured level with success message": {
giveRequest: httptest.NewRequest(http.MethodGet, "/old", nil),
giveDefaultLevel: logger.InfoLevel,
giveHandler: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusMovedPermanently) },
checkLogEntry: func(t *testing.T, rec *logger.Recorder) {
assert.Equal(t, 1, rec.Len())
e := rec.Records()[0]
assert.Equal(t, logger.InfoLevel, e.Level)
assert.Contains(t, e.Message, "Redirection")
assert.Contains(t, e.Message, "successfully processed")
assert.Equal(t, int64(http.StatusMovedPermanently), e.Attrs["status_code"].Int64())
},
},
"4xx: always logged at warn level regardless of configured level": {
"4xx: logged at configured level with success message": {
giveRequest: httptest.NewRequest(http.MethodPost, "/protected", nil),
giveDefaultLevel: logger.DebugLevel,
giveHandler: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) },
checkLogEntry: func(t *testing.T, rec *logger.Recorder) {
assert.Equal(t, 1, rec.Len())
e := rec.Records()[0]
assert.Equal(t, logger.WarnLevel, e.Level)
assert.Contains(t, e.Message, "Client error")
assert.Equal(t, logger.DebugLevel, e.Level)
assert.Contains(t, e.Message, "successfully processed")
assert.Equal(t, int64(http.StatusUnauthorized), e.Attrs["status_code"].Int64())
},
},
"5xx: always logged at error level regardless of configured level": {
"5xx: logged at configured level with success message": {
giveRequest: httptest.NewRequest(http.MethodGet, "/crash", nil),
giveDefaultLevel: logger.DebugLevel,
giveHandler: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) },
checkLogEntry: func(t *testing.T, rec *logger.Recorder) {
assert.Equal(t, 1, rec.Len())
e := rec.Records()[0]
assert.Equal(t, logger.ErrorLevel, e.Level)
assert.Contains(t, e.Message, "Server error")
assert.Equal(t, logger.DebugLevel, e.Level)
assert.Contains(t, e.Message, "successfully processed")
assert.Equal(t, int64(http.StatusInternalServerError), e.Attrs["status_code"].Int64())
},
},
Expand Down
2 changes: 1 addition & 1 deletion internal/httpserver/middleware/inject_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
// injectLogOnceCtxKey is a context key type for the inject log middleware to prevent duplicate injection.
type injectLogOnceCtxKey struct{}

// NewInjectLog returns a new middleware that injects the provided zap.Logger into the request context.
// NewInjectLog returns a new middleware that injects the provided logger.Logger into the request context.
//
// This allows downstream handlers to retrieve the logger from the context and have access to these fields for
// logging purposes.
Expand Down
2 changes: 1 addition & 1 deletion internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (l *Logger) With(f ...Attr) *Logger {

// Log logs a message at the given level. Use it directly when the level is determined at runtime.
//
//nolint:contextcheck,nolintlint // zap-like API: context is intentionally not threaded through log calls
//nolint:contextcheck,nolintlint // context is intentionally not threaded through log calls
func (l *Logger) Log(level Level, msg string, f ...Attr) {
slogLevel := slog.Level(level)

Expand Down