diff --git a/packages/shared/pkg/middleware/logging.go b/packages/shared/pkg/middleware/logging.go index 5412e5e2cc..27f7fe9733 100644 --- a/packages/shared/pkg/middleware/logging.go +++ b/packages/shared/pkg/middleware/logging.go @@ -33,6 +33,25 @@ type Config struct { Skipper Skipper } +// levelForStatus maps an HTTP status to the log level its access-log line is +// emitted at. 5xx is a server fault (Error). A 404 is the client asking for a +// route that does not exist; on a public endpoint that is dominated by bots and +// scanners probing paths like /.env or /HNAP1, so it is logged at Info to keep +// it out of the WARN stream that actionable 4xx (401/403/429/validation) use. +// Other 4xx stay at Warn. Anything below 400 uses the configured default. +func levelForStatus(status int, def zapcore.Level) zapcore.Level { + switch { + case status >= http.StatusInternalServerError: + return zapcore.ErrorLevel + case status == http.StatusNotFound: + return zapcore.InfoLevel + case status >= http.StatusBadRequest: + return zapcore.WarnLevel + default: + return def + } +} + func LoggingMiddleware(logger logger.Logger, conf Config) gin.HandlerFunc { skipPaths := make(map[string]bool, len(conf.SkipPaths)) for _, path := range conf.SkipPaths { @@ -107,15 +126,7 @@ func LoggingMiddleware(logger logger.Logger, conf Config) gin.HandlerFunc { fields = append(fields, zap.Error(errors.Join(errs...))) } - level := conf.DefaultLevel - switch { - case status >= http.StatusInternalServerError: - level = zapcore.ErrorLevel - case status >= http.StatusBadRequest: - level = zapcore.WarnLevel - } - - logger.Log(ctx, level, path, fields...) + logger.Log(ctx, levelForStatus(status, conf.DefaultLevel), path, fields...) } } -} +} \ No newline at end of file diff --git a/packages/shared/pkg/middleware/logging_test.go b/packages/shared/pkg/middleware/logging_test.go new file mode 100644 index 0000000000..969aeb215c --- /dev/null +++ b/packages/shared/pkg/middleware/logging_test.go @@ -0,0 +1,35 @@ +package middleware + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" +) + +func TestLevelForStatus(t *testing.T) { + t.Parallel() + + const def = zapcore.InfoLevel + + tests := []struct { + status int + want zapcore.Level + }{ + {http.StatusOK, def}, + {http.StatusNoContent, def}, + {http.StatusMovedPermanently, def}, + {http.StatusNotFound, zapcore.InfoLevel}, + {http.StatusBadRequest, zapcore.WarnLevel}, + {http.StatusUnauthorized, zapcore.WarnLevel}, + {http.StatusForbidden, zapcore.WarnLevel}, + {http.StatusTooManyRequests, zapcore.WarnLevel}, + {http.StatusInternalServerError, zapcore.ErrorLevel}, + {http.StatusBadGateway, zapcore.ErrorLevel}, + } + + for _, tt := range tests { + require.Equal(t, tt.want, levelForStatus(tt.status, def), "status %d", tt.status) + } +} \ No newline at end of file