Skip to content
Open
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
5 changes: 5 additions & 0 deletions json.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"bytes"
"encoding/json"
"fmt"
"log/slog"

Check failure on line 7 in json.go

View workflow job for this annotation

GitHub Actions / build (1.19) / build (ubuntu-latest)

package log/slog is not in GOROOT (/opt/hostedtoolcache/go/1.19.13/x64/src/log/slog)
"time"
)

Expand Down Expand Up @@ -38,6 +39,10 @@
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 {
Expand Down
19 changes: 19 additions & 0 deletions json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 11 additions & 0 deletions level.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import (
"errors"
"fmt"
"log/slog"
"math"
"strings"
)

// Level is a logging level.
type Level int

func (l Level) Level() slog.Level { return slog.Level(l) }

Check failure on line 14 in level.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

exported: exported method Level.Level should have comment or be unexported (revive)

Check failure on line 14 in level.go

View workflow job for this annotation

GitHub Actions / lint / lint (ubuntu-latest)

exported: exported method Level.Level should have comment or be unexported (revive)

const (
// DebugLevel is the debug level.
DebugLevel Level = -4
Expand All @@ -25,6 +28,14 @@
noLevel Level = math.MaxInt
)

var Levels map[int]fmt.Stringer = map[int]fmt.Stringer{

Check failure on line 31 in level.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

exported: exported var Levels should have comment or be unexported (revive)

Check failure on line 31 in level.go

View workflow job for this annotation

GitHub Actions / lint / lint (ubuntu-latest)

exported: exported var Levels should have comment or be unexported (revive)
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
Expand Down
21 changes: 11 additions & 10 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"bytes"
"fmt"
"io"
"log/slog"
"os"
"runtime"
"strings"
Expand All @@ -30,7 +31,7 @@

isDiscard uint32

level int64
level *slog.LevelVar
prefix string
timeFunc TimeFunction
timeFormat string
Expand All @@ -48,18 +49,18 @@
}

// 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
}

Expand All @@ -81,13 +82,13 @@
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)
}
Expand Down Expand Up @@ -136,7 +137,7 @@
}

// WriteTo will reset the buffer
l.b.WriteTo(l.w) //nolint: errcheck

Check failure on line 140 in logger.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

G104: Errors unhandled (gosec)

Check failure on line 140 in logger.go

View workflow job for this annotation

GitHub Actions / lint / lint (ubuntu-latest)

G104: Errors unhandled (gosec)
}

// Helper marks the calling function as a helper
Expand Down Expand Up @@ -226,17 +227,17 @@
}

// 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()
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type change from Level to slog.Leveler is a breaking API change. The method now returns l.level.Level() instead of the *slog.LevelVar itself, which may not be what callers expect when they need to modify the level dynamically.

Copilot uses AI. Check for mistakes.
}

// 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()
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter type change from Level to slog.Leveler is a breaking API change. Consider maintaining backward compatibility or providing clear migration guidance.

Copilot uses AI. Check for mistakes.
atomic.StoreInt64(&l.level, int64(level))
l.level.Set(level.Level())
}

// GetPrefix returns the current prefix.
Expand Down
5 changes: 2 additions & 3 deletions logger_121.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"context"
"log/slog"
"runtime"
"sync/atomic"
)

// type aliases for slog.
Expand All @@ -17,7 +16,7 @@
slogLogValuer = slog.LogValuer
)

var slogAnyValue = slog.AnyValue

Check failure on line 19 in logger_121.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

var slogAnyValue is unused (unused)

Check failure on line 19 in logger_121.go

View workflow job for this annotation

GitHub Actions / lint / lint (ubuntu-latest)

var slogAnyValue is unused (unused)

const slogKindGroup = slog.KindGroup

Expand All @@ -25,7 +24,7 @@
//
// 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.
Expand All @@ -44,7 +43,7 @@
// 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
}

Expand Down
2 changes: 1 addition & 1 deletion logger_no121.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
27 changes: 26 additions & 1 deletion logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"bytes"
"fmt"
"io"
"log/slog"
"strings"
"sync"
"testing"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -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())
}
3 changes: 2 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package log

import (
"fmt"
"log/slog"
"time"
)

Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 15 additions & 3 deletions pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"log"
"log/slog"
"os"
"sync"
"sync/atomic"
Expand Down Expand Up @@ -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,
Expand All @@ -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
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a type switch on an interface without handling the nil case explicitly could lead to unexpected behavior. Consider adding a nil check before the type switch.

Suggested change
l.level = lvl
if o.Level == nil {
lvl := new(slog.LevelVar)
lvl.Set(slog.LevelInfo)
l.level = lvl
} else {
switch lvlVar := o.Level.(type) {
case *slog.LevelVar:
l.level = lvlVar
default:
lvl := new(slog.LevelVar)
lvl.Set(o.Level.Level())
l.level = lvl
}

Copilot uses AI. Check for mistakes.
}
l.SetStyles(DefaultStyles())

if l.callerFormatter == nil {
Expand Down Expand Up @@ -99,7 +111,7 @@ func SetLevel(level Level) {
}

// GetLevel returns the level for the default logger.
func GetLevel() Level {
func GetLevel() slog.Leveler {
Copy link

Copilot AI Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type change from Level to slog.Leveler is a breaking API change. Consider adding a deprecation notice or maintaining backward compatibility by providing both functions.

Suggested change
func GetLevel() slog.Leveler {
//
// Deprecated: Use GetLeveler() instead. This will be removed in a future release.
func GetLevel() Level {
return Default().GetLevel().Level()
}
// GetLeveler returns the slog.Leveler for the default logger.
func GetLeveler() slog.Leveler {

Copilot uses AI. Check for mistakes.
return Default().GetLevel()
}

Expand Down
3 changes: 2 additions & 1 deletion pkg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 7 additions & 7 deletions styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
Expand Down
5 changes: 3 additions & 2 deletions text.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import (
"fmt"
"io"
"log/slog"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -160,7 +161,7 @@

func writeSpace(w io.Writer, first bool) {
if !first {
w.Write([]byte{' '}) //nolint: errcheck

Check failure on line 164 in text.go

View workflow job for this annotation

GitHub Actions / lint / lint (macos-latest)

G104: Errors unhandled (gosec)

Check failure on line 164 in text.go

View workflow job for this annotation

GitHub Actions / lint / lint (ubuntu-latest)

G104: Errors unhandled (gosec)
}
}

Expand All @@ -181,9 +182,9 @@
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
}
Expand Down
Loading
Loading