From 1b15872173c3cbbc81138da1561518dd05ddc825 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 13 Aug 2026 11:52:06 +0200 Subject: [PATCH 1/4] slog: rewrite level synchronization with Logrus Use the Logrus slog level adapter as a dynamic slog.Leveler instead of maintaining a separate slog.LevelVar and duplicating the Logrus-to-slog level mapping. This keeps the Logrus logger as the single source of truth for the configured level, so changes made through SetLevel or directly on L.Logger are reflected by slog handlers automatically. Also avoid formatting Logrus entries that are forwarded to slog and then discarded. Signed-off-by: Sebastiaan van Stijn --- context.go | 7 ++----- slog.go | 57 +++++++++++++--------------------------------------- slog_test.go | 3 ++- 3 files changed, 18 insertions(+), 49 deletions(-) diff --git a/context.go b/context.go index 3b1ca86..0e2b573 100644 --- a/context.go +++ b/context.go @@ -128,9 +128,6 @@ func SetLevel(level string) error { } L.Logger.SetLevel(lvl) - if slogOut != nil { - slogLevel.Set(logrusToSlogLevel(lvl)) - } return nil } @@ -171,9 +168,9 @@ func SetFormat(format OutputFormat) error { var handler slog.Handler switch format { case TextFormat: - handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: slogLevel}) + handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}}) case JSONFormat: - handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: slogLevel}) + handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}}) } slog.SetDefault(slog.New(handler)) } diff --git a/slog.go b/slog.go index dc1db2a..6e230ca 100644 --- a/slog.go +++ b/slog.go @@ -17,20 +17,17 @@ package log import ( - "context" "io" "log/slog" "sync" "github.com/sirupsen/logrus" + lslog "github.com/sirupsen/logrus/hooks/slog" ) // slogOut is used to set the slog logger when setting output format. var slogOut io.Writer -// slogLevel is used to control the slog handler's level when slog output is active. -var slogLevel = &slog.LevelVar{} - // slogOnce guards UseSlog so repeated calls do not stack up hooks or // reset slogOut to the discard writer installed on the first call. var slogOnce sync.Once @@ -40,56 +37,30 @@ func UseSlog() { L.Logger.SetNoLock() L.Logger.AddHook(slogHook{}) slogOut = L.Logger.Out + + // Disable logrus formatting and output, as both are handled by slog. + L.Logger.SetFormatter(discardFormatter{}) L.Logger.SetOutput(io.Discard) - slogLevel.Set(logrusToSlogLevel(L.Logger.GetLevel())) }) } type slogHook struct{} -func (hook slogHook) Levels() []logrus.Level { +func (slogHook) Levels() []logrus.Level { return logrus.AllLevels } -func logrusToSlogLevel(l logrus.Level) slog.Level { - switch l { - case logrus.PanicLevel: - return slog.LevelError + 4 - case logrus.FatalLevel: - return slog.LevelError + 2 - case logrus.ErrorLevel: - return slog.LevelError - case logrus.WarnLevel: - return slog.LevelWarn - case logrus.DebugLevel: - return slog.LevelDebug - case logrus.TraceLevel: - return slog.LevelDebug - 4 - default: - return slog.LevelInfo - } +func (slogHook) Fire(entry *logrus.Entry) error { + return lslog.NewHook(slog.Default(), nil).Fire(entry) } -func (hook slogHook) Fire(entry *logrus.Entry) error { - level := logrusToSlogLevel(entry.Level) - - handler := slog.Default().Handler() - - ctx := entry.Context - if ctx == nil { - ctx = context.Background() - } +// loggerLevel exposes the current Logrus logger level as a slog.Leveler. +type loggerLevel struct{} - if !handler.Enabled(ctx, level) { - return nil - } - - record := slog.NewRecord(entry.Time, level, entry.Message, 0) +func (loggerLevel) Level() slog.Level { + return lslog.Level(L.Logger.GetLevel()).Level() +} - // Convert logrus fields to slog attributes. - for k, v := range entry.Data { - record.AddAttrs(slog.Any(k, v)) - } +type discardFormatter struct{} - return handler.Handle(ctx, record) -} +func (discardFormatter) Format(*logrus.Entry) ([]byte, error) { return nil, nil } diff --git a/slog_test.go b/slog_test.go index 950978f..bf6de24 100644 --- a/slog_test.go +++ b/slog_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/sirupsen/logrus" + lslog "github.com/sirupsen/logrus/hooks/slog" ) // setupSlogTest sets up UseSlogHook with a captured slog buffer and a separate @@ -266,7 +267,7 @@ func TestLogrusToSlogLevel(t *testing.T) { for _, tc := range tests { t.Run(tc.logrusLevel.String(), func(t *testing.T) { - got := logrusToSlogLevel(tc.logrusLevel) + got := lslog.Level(tc.logrusLevel).Level() if got != tc.slogLevel { t.Errorf("logrusToSlogLevel(%v) = %v, want %v", tc.logrusLevel, got, tc.slogLevel) } From 830d81975a74094034cd27f895b247385116e468 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 14 Aug 2026 20:15:22 +0200 Subject: [PATCH 2/4] log: keep logrus disabled when setting slog format When slog output is enabled, SetFormat reconfigures the slog handler but could leave logrus formatting or output enabled if either had been changed after UseSlog. Reset the logrus formatter and output when updating the slog format so logrus remains only the forwarding path and does not duplicate work or output. Signed-off-by: Sebastiaan van Stijn --- context.go | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/context.go b/context.go index 0e2b573..f76deca 100644 --- a/context.go +++ b/context.go @@ -40,6 +40,7 @@ package log import ( "context" "fmt" + "io" "log/slog" "github.com/sirupsen/logrus" @@ -150,6 +151,24 @@ const ( // SetFormat sets the log output format ([TextFormat] or [JSONFormat]). func SetFormat(format OutputFormat) error { + if slogOut != nil { + var handler slog.Handler + switch format { + case TextFormat: + handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}}) + case JSONFormat: + handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}}) + default: + return fmt.Errorf("unknown log format: %s", format) + } + slog.SetDefault(slog.New(handler)) + + // Keep logrus formatting and output disabled, as both are handled by slog. + L.Logger.SetFormatter(discardFormatter{}) + L.Logger.SetOutput(io.Discard) + return nil + } + switch format { case TextFormat: L.Logger.SetFormatter(&logrus.TextFormatter{ @@ -164,17 +183,6 @@ func SetFormat(format OutputFormat) error { return fmt.Errorf("unknown log format: %s", format) } - if slogOut != nil { - var handler slog.Handler - switch format { - case TextFormat: - handler = slog.NewTextHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}}) - case JSONFormat: - handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: loggerLevel{}}) - } - slog.SetDefault(slog.New(handler)) - } - return nil } From d647ed8321906cccb7ff286dcf8df29fa90b0a70 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 13 Aug 2026 12:32:38 +0200 Subject: [PATCH 3/4] slog: remove duplicate level mapping test Remove TestLogrusToSlogLevel now that containerd uses the level adapter from logrus/hooks/slog directly. The mapping itself is covered by the Logrus package, while containerd's integration tests verify that forwarded records use the expected slog levels and that filtering follows the configured Logrus level. Signed-off-by: Sebastiaan van Stijn --- slog_test.go | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/slog_test.go b/slog_test.go index bf6de24..2ba8add 100644 --- a/slog_test.go +++ b/slog_test.go @@ -25,7 +25,6 @@ import ( "testing" "github.com/sirupsen/logrus" - lslog "github.com/sirupsen/logrus/hooks/slog" ) // setupSlogTest sets up UseSlogHook with a captured slog buffer and a separate @@ -250,27 +249,3 @@ func TestSetLevelWithSlog(t *testing.T) { t.Errorf("expected info message to appear at debug level, got: %s", slogBuf.String()) } } - -func TestLogrusToSlogLevel(t *testing.T) { - tests := []struct { - logrusLevel logrus.Level - slogLevel slog.Level - }{ - {logrus.PanicLevel, slog.LevelError + 4}, - {logrus.FatalLevel, slog.LevelError + 2}, - {logrus.ErrorLevel, slog.LevelError}, - {logrus.WarnLevel, slog.LevelWarn}, - {logrus.InfoLevel, slog.LevelInfo}, - {logrus.DebugLevel, slog.LevelDebug}, - {logrus.TraceLevel, slog.LevelDebug - 4}, - } - - for _, tc := range tests { - t.Run(tc.logrusLevel.String(), func(t *testing.T) { - got := lslog.Level(tc.logrusLevel).Level() - if got != tc.slogLevel { - t.Errorf("logrusToSlogLevel(%v) = %v, want %v", tc.logrusLevel, got, tc.slogLevel) - } - }) - } -} From 683ef7d7788b3a076953ec2cc610ba05ca02ba9c Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 13 Aug 2026 12:30:32 +0200 Subject: [PATCH 4/4] slog: improve bridge test coverage Verify that Logrus records are forwarded with the expected slog levels, and that slog filtering follows changes to the Logrus logger level. Cover both SetLevel and direct changes to L.Logger to ensure the dynamic level adapter remains in sync without separate level state. Signed-off-by: Sebastiaan van Stijn --- slog_test.go | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/slog_test.go b/slog_test.go index 2ba8add..a79ed91 100644 --- a/slog_test.go +++ b/slog_test.go @@ -141,15 +141,16 @@ func TestUseSlogHookLevels(t *testing.T) { slogBuf, logrusBuf := setupSlogTest(t) tests := []struct { - name string - logFunc func(string, ...any) - message string + name string + logFunc func(string, ...any) + message string + wantLevel string }{ - {"trace", L.Tracef, "trace-msg"}, - {"debug", L.Debugf, "debug-msg"}, - {"info", L.Infof, "info-msg"}, - {"warn", L.Warnf, "warn-msg"}, - {"error", L.Errorf, "error-msg"}, + {"trace", L.Tracef, "trace-msg", "DEBUG-4"}, + {"debug", L.Debugf, "debug-msg", "DEBUG"}, + {"info", L.Infof, "info-msg", "INFO"}, + {"warn", L.Warnf, "warn-msg", "WARN"}, + {"error", L.Errorf, "error-msg", "ERROR"}, } for _, tc := range tests { @@ -159,8 +160,12 @@ func TestUseSlogHookLevels(t *testing.T) { tc.logFunc(tc.message) - if !strings.Contains(slogBuf.String(), tc.message) { - t.Errorf("expected slog output to contain %q, got: %s", tc.message, slogBuf.String()) + output := slogBuf.String() + if !strings.Contains(output, tc.message) { + t.Errorf("expected slog output to contain %q, got: %s", tc.message, output) + } + if !strings.Contains(output, "level="+tc.wantLevel) { + t.Errorf("expected slog output to contain level %q, got: %s", tc.wantLevel, output) } if logrusBuf.Len() != 0 { t.Errorf("expected no logrus output, got: %s", logrusBuf.String()) @@ -212,6 +217,8 @@ func TestSetFormatWithSlog(t *testing.T) { }) } +// TestSetLevelWithSlog verifies that slog filtering follows the Logrus logger +// level, whether changed through SetLevel or directly on the logger. func TestSetLevelWithSlog(t *testing.T) { slogBuf, _ := setupSlogTest(t) @@ -248,4 +255,21 @@ func TestSetLevelWithSlog(t *testing.T) { if !strings.Contains(slogBuf.String(), "now visible") { t.Errorf("expected info message to appear at debug level, got: %s", slogBuf.String()) } + + // Direct changes to the Logrus logger should also affect slog filtering. + L.Logger.SetLevel(logrus.WarnLevel) + + slogBuf.Reset() + L.Info("hidden after direct change") + if slogBuf.Len() != 0 { + t.Errorf("expected info message to be suppressed after direct level change, got: %s", slogBuf.String()) + } + + L.Logger.SetLevel(logrus.DebugLevel) + + slogBuf.Reset() + L.Info("visible after direct change") + if !strings.Contains(slogBuf.String(), "visible after direct change") { + t.Errorf("expected info message to appear after direct level change, got: %s", slogBuf.String()) + } }