diff --git a/context.go b/context.go index 3b1ca86..f76deca 100644 --- a/context.go +++ b/context.go @@ -40,6 +40,7 @@ package log import ( "context" "fmt" + "io" "log/slog" "github.com/sirupsen/logrus" @@ -128,9 +129,6 @@ func SetLevel(level string) error { } L.Logger.SetLevel(lvl) - if slogOut != nil { - slogLevel.Set(logrusToSlogLevel(lvl)) - } return nil } @@ -153,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{ @@ -167,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: slogLevel}) - case JSONFormat: - handler = slog.NewJSONHandler(slogOut, &slog.HandlerOptions{Level: slogLevel}) - } - slog.SetDefault(slog.New(handler)) - } - return nil } 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..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,28 +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()) } -} -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}, + // 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()) } - for _, tc := range tests { - t.Run(tc.logrusLevel.String(), func(t *testing.T) { - got := logrusToSlogLevel(tc.logrusLevel) - if got != tc.slogLevel { - t.Errorf("logrusToSlogLevel(%v) = %v, want %v", tc.logrusLevel, got, tc.slogLevel) - } - }) + 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()) } }