From 056f91ef47d0e3b718876f988c202651ceb4f7c7 Mon Sep 17 00:00:00 2001 From: Konstantin Kondr <2929477+kondr1@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:18:10 +0300 Subject: [PATCH] Implement use slog.Leveler and slog.LevelVar. And adding custom log levels --- json.go | 5 +++++ json_test.go | 19 +++++++++++++++++++ level.go | 11 +++++++++++ logger.go | 21 +++++++++++---------- logger_121.go | 5 ++--- logger_no121.go | 2 +- logger_test.go | 27 ++++++++++++++++++++++++++- options.go | 3 ++- options_test.go | 2 +- pkg.go | 18 +++++++++++++++--- pkg_test.go | 3 ++- styles.go | 14 +++++++------- text.go | 5 +++-- text_test.go | 26 +++++++++++++------------- 14 files changed, 118 insertions(+), 43 deletions(-) diff --git a/json.go b/json.go index f84083d..7110e97 100644 --- a/json.go +++ b/json.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "log/slog" "time" ) @@ -38,6 +39,10 @@ func (l *Logger) jsonFormatterRoot(jw *jsonWriter, key, value any) { case LevelKey: if level, ok := value.(Level); ok { jw.objectItem(LevelKey, level.String()) + } else if level, ok := value.(slog.Leveler); ok { + if lvl, ok := Levels[int(level.Level())]; ok { + jw.objectItem(LevelKey, lvl.String()) + } } case CallerKey: if caller, ok := value.(string); ok { diff --git a/json_test.go b/json_test.go index 21f14c3..9f5ea7c 100644 --- a/json_test.go +++ b/json_test.go @@ -6,11 +6,30 @@ import ( "fmt" "path/filepath" "runtime" + "strings" "testing" + "github.com/charmbracelet/lipgloss" "github.com/stretchr/testify/require" ) +func TestJsonCustomLevelWithStyle(t *testing.T) { + var buf bytes.Buffer + l := New(&buf) + styles := DefaultStyles() + Levels[int(Critical)] = Critical + styles.Levels[int(Critical)] = lipgloss.NewStyle(). + SetString(strings.ToUpper(Critical.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.Color("134")) + l.SetStyles(styles) + l.SetLevel(InfoLevel) + l.SetFormatter(JSONFormatter) + l.Logf(Critical, "foo") + require.Equal(t, "{\"level\":\"crit\",\"msg\":\"foo\"}\n", buf.String()) +} + func TestJson(t *testing.T) { var buf bytes.Buffer l := New(&buf) diff --git a/level.go b/level.go index 6876e34..ba3cf76 100644 --- a/level.go +++ b/level.go @@ -3,6 +3,7 @@ package log import ( "errors" "fmt" + "log/slog" "math" "strings" ) @@ -10,6 +11,8 @@ import ( // Level is a logging level. type Level int +func (l Level) Level() slog.Level { return slog.Level(l) } + const ( // DebugLevel is the debug level. DebugLevel Level = -4 @@ -25,6 +28,14 @@ const ( noLevel Level = math.MaxInt ) +var Levels map[int]fmt.Stringer = map[int]fmt.Stringer{ + int(DebugLevel): DebugLevel, + int(InfoLevel): InfoLevel, + int(WarnLevel): WarnLevel, + int(ErrorLevel): ErrorLevel, + int(FatalLevel): FatalLevel, +} + // String returns the string representation of the level. func (l Level) String() string { switch l { //nolint:exhaustive diff --git a/logger.go b/logger.go index 19ed409..c567f6a 100644 --- a/logger.go +++ b/logger.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "log/slog" "os" "runtime" "strings" @@ -30,7 +31,7 @@ type Logger struct { isDiscard uint32 - level int64 + level *slog.LevelVar prefix string timeFunc TimeFunction timeFormat string @@ -48,18 +49,18 @@ type Logger struct { } // Logf logs a message with formatting. -func (l *Logger) Logf(level Level, format string, args ...interface{}) { +func (l *Logger) Logf(level slog.Leveler, format string, args ...interface{}) { l.Log(level, fmt.Sprintf(format, args...)) } // Log logs the given message with the given keyvals for the given level. -func (l *Logger) Log(level Level, msg interface{}, keyvals ...interface{}) { +func (l *Logger) Log(level slog.Leveler, msg interface{}, keyvals ...interface{}) { if atomic.LoadUint32(&l.isDiscard) != 0 { return } // check if the level is allowed - if atomic.LoadInt64(&l.level) > int64(level) { + if l.level.Level() > level.Level() { return } @@ -81,13 +82,13 @@ func (l *Logger) Log(level Level, msg interface{}, keyvals ...interface{}) { l.handle(level, l.timeFunc(time.Now()), []runtime.Frame{frame}, msg, keyvals...) } -func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg interface{}, keyvals ...interface{}) { +func (l *Logger) handle(level slog.Leveler, ts time.Time, frames []runtime.Frame, msg interface{}, keyvals ...interface{}) { var kvs []interface{} if l.reportTimestamp && !ts.IsZero() { kvs = append(kvs, TimestampKey, ts) } - _, ok := l.styles.Levels[level] + _, ok := l.styles.Levels[int(level.Level())] if ok { kvs = append(kvs, LevelKey, level) } @@ -226,17 +227,17 @@ func (l *Logger) SetReportCaller(report bool) { } // GetLevel returns the current level. -func (l *Logger) GetLevel() Level { +func (l *Logger) GetLevel() slog.Leveler { l.mu.RLock() defer l.mu.RUnlock() - return Level(l.level) + return l.level.Level() } // SetLevel sets the current level. -func (l *Logger) SetLevel(level Level) { +func (l *Logger) SetLevel(level slog.Leveler) { l.mu.Lock() defer l.mu.Unlock() - atomic.StoreInt64(&l.level, int64(level)) + l.level.Set(level.Level()) } // GetPrefix returns the current prefix. diff --git a/logger_121.go b/logger_121.go index 478e1a0..999139e 100644 --- a/logger_121.go +++ b/logger_121.go @@ -7,7 +7,6 @@ import ( "context" "log/slog" "runtime" - "sync/atomic" ) // type aliases for slog. @@ -25,7 +24,7 @@ const slogKindGroup = slog.KindGroup // // Implements slog.Handler. func (l *Logger) Enabled(_ context.Context, level slog.Level) bool { - return atomic.LoadInt64(&l.level) <= int64(level) + return l.level.Level() <= level } // Handle handles the Record. It will only be called if Enabled returns true. @@ -44,7 +43,7 @@ func (l *Logger) Handle(ctx context.Context, record slog.Record) error { // Get the caller frame using the record's PC. frames := runtime.CallersFrames([]uintptr{record.PC}) frame, _ := frames.Next() - l.handle(Level(record.Level), l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...) + l.handle(record.Level, l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...) return nil } diff --git a/logger_no121.go b/logger_no121.go index ea8bb10..16add33 100644 --- a/logger_no121.go +++ b/logger_no121.go @@ -41,7 +41,7 @@ func (l *Logger) Handle(_ context.Context, record slog.Record) error { // Get the caller frame using the record's PC. frames := runtime.CallersFrames([]uintptr{record.PC}) frame, _ := frames.Next() - l.handle(Level(record.Level), l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...) + l.handle(record.Level, l.timeFunc(record.Time), []runtime.Frame{frame}, record.Message, fields...) return nil } diff --git a/logger_test.go b/logger_test.go index d46facc..b81ddd4 100644 --- a/logger_test.go +++ b/logger_test.go @@ -4,10 +4,13 @@ import ( "bytes" "fmt" "io" + "log/slog" + "strings" "sync" "testing" "time" + "github.com/charmbracelet/lipgloss" "github.com/stretchr/testify/assert" ) @@ -279,9 +282,31 @@ func TestRace(t *testing.T) { func TestCustomLevel(t *testing.T) { var buf bytes.Buffer - level500 := Level(500) + level500 := slog.Level(500) l := New(&buf) l.SetLevel(level500) l.Logf(level500, "foo") assert.Equal(t, "foo\n", buf.String()) } + +type CriticalLevel int + +func (l CriticalLevel) Level() slog.Level { return slog.Level(l) } +func (l CriticalLevel) String() string { return "crit" } + +const Critical CriticalLevel = 600 + +func TestCustomLevelWithStyle(t *testing.T) { + var buf bytes.Buffer + l := New(&buf) + styles := DefaultStyles() + styles.Levels[int(Critical)] = lipgloss.NewStyle(). + SetString(strings.ToUpper(Critical.String())). + Bold(true). + MaxWidth(4). + Foreground(lipgloss.Color("134")) + l.SetStyles(styles) + l.SetLevel(InfoLevel) + l.Logf(Critical, "foo") + assert.Equal(t, "CRIT foo\n", buf.String()) +} diff --git a/options.go b/options.go index 8737d7d..2e85da9 100644 --- a/options.go +++ b/options.go @@ -2,6 +2,7 @@ package log import ( "fmt" + "log/slog" "time" ) @@ -43,7 +44,7 @@ type Options struct { // TimeFormat is the time format for the logger. The default is "2006/01/02 15:04:05". TimeFormat string // Level is the level for the logger. The default is InfoLevel. - Level Level + Level slog.Leveler // Prefix is the prefix for the logger. The default is no prefix. Prefix string // ReportTimestamp is whether the logger should report the timestamp. The default is false. diff --git a/options_test.go b/options_test.go index 9262a01..6be152c 100644 --- a/options_test.go +++ b/options_test.go @@ -16,7 +16,7 @@ func TestOptions(t *testing.T) { Fields: []interface{}{"foo", "bar"}, } logger := NewWithOptions(io.Discard, opts) - require.Equal(t, ErrorLevel, logger.GetLevel()) + require.Equal(t, opts.Level, Level(logger.GetLevel().Level())) require.True(t, logger.reportCaller) require.False(t, logger.reportTimestamp) require.Equal(t, []interface{}{"foo", "bar"}, logger.fields) diff --git a/pkg.go b/pkg.go index 712bb38..158125d 100644 --- a/pkg.go +++ b/pkg.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "log" + "log/slog" "os" "sync" "sync/atomic" @@ -52,7 +53,7 @@ func NewWithOptions(w io.Writer, o Options) *Logger { b: bytes.Buffer{}, mu: &sync.RWMutex{}, helpers: &sync.Map{}, - level: int64(o.Level), + level: nil, reportTimestamp: o.ReportTimestamp, reportCaller: o.ReportCaller, prefix: o.Prefix, @@ -65,7 +66,18 @@ func NewWithOptions(w io.Writer, o Options) *Logger { } l.SetOutput(w) - l.SetLevel(Level(l.level)) + switch o.Level.(type) { + case *slog.LevelVar: + l.level = o.Level.(*slog.LevelVar) + default: + lvl := new(slog.LevelVar) + if o.Level == nil { + lvl.Set(slog.LevelInfo) + } else { + lvl.Set(o.Level.Level()) + } + l.level = lvl + } l.SetStyles(DefaultStyles()) if l.callerFormatter == nil { @@ -99,7 +111,7 @@ func SetLevel(level Level) { } // GetLevel returns the level for the default logger. -func GetLevel() Level { +func GetLevel() slog.Leveler { return Default().GetLevel() } diff --git a/pkg_test.go b/pkg_test.go index 0a4e844..6f43425 100644 --- a/pkg_test.go +++ b/pkg_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "log/slog" "os" "os/exec" "path/filepath" @@ -197,7 +198,7 @@ func TestWith(t *testing.T) { func TestGetLevel(t *testing.T) { SetLevel(InfoLevel) - assert.Equal(t, InfoLevel, GetLevel()) + assert.Equal(t, slog.Level(InfoLevel), GetLevel()) } func TestPrefix(t *testing.T) { diff --git a/styles.go b/styles.go index 5a4e545..b06d945 100644 --- a/styles.go +++ b/styles.go @@ -30,7 +30,7 @@ type Styles struct { Separator lipgloss.Style // Levels are the styles for each level. - Levels map[Level]lipgloss.Style + Levels map[int]lipgloss.Style // Keys overrides styles for specific keys. Keys map[string]lipgloss.Style @@ -49,28 +49,28 @@ func DefaultStyles() *Styles { Key: lipgloss.NewStyle().Faint(true), Value: lipgloss.NewStyle(), Separator: lipgloss.NewStyle().Faint(true), - Levels: map[Level]lipgloss.Style{ - DebugLevel: lipgloss.NewStyle(). + Levels: map[int]lipgloss.Style{ + int(DebugLevel): lipgloss.NewStyle(). SetString(strings.ToUpper(DebugLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("63")), - InfoLevel: lipgloss.NewStyle(). + int(InfoLevel): lipgloss.NewStyle(). SetString(strings.ToUpper(InfoLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("86")), - WarnLevel: lipgloss.NewStyle(). + int(WarnLevel): lipgloss.NewStyle(). SetString(strings.ToUpper(WarnLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("192")), - ErrorLevel: lipgloss.NewStyle(). + int(ErrorLevel): lipgloss.NewStyle(). SetString(strings.ToUpper(ErrorLevel.String())). Bold(true). MaxWidth(4). Foreground(lipgloss.Color("204")), - FatalLevel: lipgloss.NewStyle(). + int(FatalLevel): lipgloss.NewStyle(). SetString(strings.ToUpper(FatalLevel.String())). Bold(true). MaxWidth(4). diff --git a/text.go b/text.go index a3775e8..812dd97 100644 --- a/text.go +++ b/text.go @@ -3,6 +3,7 @@ package log import ( "fmt" "io" + "log/slog" "strings" "sync" "time" @@ -181,9 +182,9 @@ func (l *Logger) textFormatter(keyvals ...interface{}) { l.b.WriteString(ts) } case LevelKey: - if level, ok := keyvals[i+1].(Level); ok { + if level, ok := keyvals[i+1].(slog.Leveler); ok { var lvl string - lvlStyle, ok := st.Levels[level] + lvlStyle, ok := st.Levels[int(level.Level())] if !ok { continue } diff --git a/text_test.go b/text_test.go index 12220ce..c4a0814 100644 --- a/text_test.go +++ b/text_test.go @@ -260,7 +260,7 @@ func TestTextValueStyles(t *testing.T) { }{ { name: "simple message", - expected: fmt.Sprintf("%s info\n", st.Levels[InfoLevel]), + expected: fmt.Sprintf("%s info\n", st.Levels[int(InfoLevel)]), msg: "info", kvs: nil, f: logger.Info, @@ -276,7 +276,7 @@ func TestTextValueStyles(t *testing.T) { name: "message with keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s\n", - st.Levels[InfoLevel], + st.Levels[int(InfoLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), ), @@ -288,7 +288,7 @@ func TestTextValueStyles(t *testing.T) { name: "error message with multiline", expected: fmt.Sprintf( "%s info\n %s%s\n%s%s\n%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Separator.Render(indentSeparator), st.Value.Render("val1"), st.Separator.Render(indentSeparator), st.Value.Render("val2"), @@ -301,7 +301,7 @@ func TestTextValueStyles(t *testing.T) { name: "error message with keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), ), @@ -313,7 +313,7 @@ func TestTextValueStyles(t *testing.T) { name: "odd number of keyvals", expected: fmt.Sprintf( "%s info %s%s%s %s%s%s %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("val1"), st.Key.Render("key2"), st.Separator.Render(separator), st.Value.Render("val2"), st.Key.Render("key3"), st.Separator.Render(separator), st.Values["key3"].Render(`"missing value"`), @@ -326,7 +326,7 @@ func TestTextValueStyles(t *testing.T) { name: "error field", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"error value"`), ), msg: "info", @@ -337,7 +337,7 @@ func TestTextValueStyles(t *testing.T) { name: "struct field", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[InfoLevel], + st.Levels[int(InfoLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render("{foo:bar}"), ), msg: "info", @@ -348,7 +348,7 @@ func TestTextValueStyles(t *testing.T) { name: "struct field quoted", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[InfoLevel], + st.Levels[int(InfoLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"{foo:bar baz}"`), ), msg: "info", @@ -359,7 +359,7 @@ func TestTextValueStyles(t *testing.T) { name: "slice of strings", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[foo bar]"`), ), msg: "info", @@ -370,7 +370,7 @@ func TestTextValueStyles(t *testing.T) { name: "slice of structs", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[{foo:bar} {foo:baz}]"`), ), msg: "info", @@ -381,7 +381,7 @@ func TestTextValueStyles(t *testing.T) { name: "slice of errors", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"[error value1 error value2]"`), ), msg: "info", @@ -392,7 +392,7 @@ func TestTextValueStyles(t *testing.T) { name: "map of strings", expected: fmt.Sprintf( "%s info %s%s%s\n", - st.Levels[ErrorLevel], + st.Levels[int(ErrorLevel)], st.Key.Render("key1"), st.Separator.Render(separator), st.Value.Render(`"map[baz:qux foo:bar]"`), ), msg: "info", @@ -428,7 +428,7 @@ func TestCustomLevelStyle(t *testing.T) { l := New(&buf) st := DefaultStyles() lvl := Level(1234) - st.Levels[lvl] = lipgloss.NewStyle().Bold(true).SetString("FUNKY") + st.Levels[int(lvl)] = lipgloss.NewStyle().Bold(true).SetString("FUNKY") l.SetStyles(st) l.SetLevel(lvl) l.Log(lvl, "foobar")