Skip to content

Commit

Permalink
feat: customize text log level styling
Browse files Browse the repository at this point in the history
  • Loading branch information
lvlcn-t committed Jul 29, 2024
1 parent 413a12c commit a544882
Show file tree
Hide file tree
Showing 10 changed files with 220 additions and 68 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/lvlcn-t/loggerhead
go 1.22

require (
github.com/charmbracelet/lipgloss v0.12.1
github.com/charmbracelet/log v0.4.0
github.com/remychantenay/slog-otel v1.3.2
go.opentelemetry.io/otel v1.28.0
Expand All @@ -12,19 +13,18 @@ require (

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.12.1 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/sys v0.22.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -48,6 +50,8 @@ go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7 h1:wDLEX9a7YQoKdKNQt88rtydkqDxeGaBUTnIYc3iG/mA=
golang.org/x/exp v0.0.0-20240716175740-e3f259677ff7/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down
85 changes: 36 additions & 49 deletions internal/logger/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,50 @@ var _ Logger = (*logger)(nil)

// Logger is a interface that provides logging methods.
type Logger interface {
// Debug logs at LevelDebug.
// Debug logs at [LevelDebug].
Debug(msg string, args ...any)
// Debugf logs at LevelDebug.
// Arguments are handled in the manner of fmt.Printf.
// Debugf logs at [LevelDebug].
// Arguments are handled in the manner of [fmt.Printf].
Debugf(msg string, args ...any)
// DebugContext logs at LevelDebug with the given context.
// DebugContext logs at [LevelDebug] with the given context.
DebugContext(ctx context.Context, msg string, args ...any)
// Info logs at LevelInfo.
// Info logs at [LevelInfo].
Info(msg string, args ...any)
// Infof logs at LevelInfo.
// Arguments are handled in the manner of fmt.Printf.
// Infof logs at [LevelInfo].
// Arguments are handled in the manner of [fmt.Printf].
Infof(msg string, args ...any)
// InfoContext logs at LevelInfo with the given context.
// InfoContext logs at [LevelInfo] with the given context.
InfoContext(ctx context.Context, msg string, args ...any)
// Warn logs at LevelWarn.
// Warn logs at [LevelWarn].
Warn(msg string, args ...any)
// Warnf logs at LevelWarn.
// Arguments are handled in the manner of fmt.Printf.
// Warnf logs at [LevelWarn].
// Arguments are handled in the manner of [fmt.Printf].
Warnf(msg string, args ...any)
// WarnContext logs at LevelWarn with the given context.
// WarnContext logs at [LevelWarn] with the given context.
WarnContext(ctx context.Context, msg string, args ...any)
// Error logs at LevelError.
// Error logs at [LevelError].
Error(msg string, args ...any)
// Errorf logs at LevelError.
// Arguments are handled in the manner of fmt.Printf.
// Errorf logs at [LevelError].
// Arguments are handled in the manner of [fmt.Printf].
Errorf(msg string, args ...any)
// ErrorContext logs at LevelError with the given context.
// ErrorContext logs at [LevelError] with the given context.
ErrorContext(ctx context.Context, msg string, args ...any)
// Panic logs at LevelPanic and then panics with the given message.
// Panic logs at [LevelPanic] and then panics with the given message.
Panic(msg string, args ...any)
// Panicf logs at LevelPanic and then panics.
// Arguments are handled in the manner of fmt.Printf.
// Panicf logs at [LevelPanic] and then panics.
// Arguments are handled in the manner of [fmt.Printf].
Panicf(msg string, args ...any)
// PanicContext logs at LevelPanic with the given context and then panics with the given message.
// PanicContext logs at [LevelPanic] with the given context and then panics with the given message.
PanicContext(ctx context.Context, msg string, args ...any)
// Fatal logs at LevelFatal and then calls os.Exit(1).
// Fatal logs at [LevelFatal] and then calls [os.Exit](1).
Fatal(msg string, args ...any)
// Fatalf logs at LevelFatal and then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Printf.
// Fatalf logs at [LevelFatal] and then calls [os.Exit](1).
// Arguments are handled in the manner of [fmt.Printf].
Fatalf(msg string, args ...any)
// FatalContext logs at LevelFatal with the given context and then calls os.Exit(1).
// FatalContext logs at [LevelFatal] with the given context and then calls [os.Exit](1).
FatalContext(ctx context.Context, msg string, args ...any)

// With calls Logger.With on the default logger.
// With returns a Logger that has the given attributes.
With(args ...any) Logger
// WithGroup returns a Logger that starts a group, if name is non-empty.
// The keys of all attributes added to the Logger will be qualified by the given
Expand All @@ -75,15 +75,15 @@ type Logger interface {
// into an Attr.
// - Otherwise, the argument is treated as a value with key "!BADKEY".
Log(ctx context.Context, level Level, msg string, args ...any)
// LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs.
// LogAttrs is a more efficient version of [Logger].Log that accepts only Attrs.
LogAttrs(ctx context.Context, level Level, msg string, attrs ...slog.Attr)

// Handler returns l's Handler.
// Handler returns the [slog.Handler] that the Logger emits log records to.
Handler() slog.Handler
// Enabled reports whether l emits log records at the given context and level.
// Enabled reports whether the [Logger] emits log records at the given context and level.
Enabled(ctx context.Context, level Level) bool

// ToSlog returns the underlying slog.Logger.
// ToSlog returns the underlying [slog.Logger].
ToSlog() *slog.Logger
}

Expand Down Expand Up @@ -152,35 +152,22 @@ func (l *logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ..
}

// logAttrs emits a log record with the current time and the given level, message, and attributes.
// Must be called by a public log method to ensure that the caller is correct.
func (l *logger) logAttrs(ctx context.Context, level Level, msg string, a ...any) {
if !l.Enabled(ctx, level) {
return
}

pc := getCaller(3)

r := slog.NewRecord(time.Now(), level, msg, pc)
// skip is the number of stack frames to skip to find the caller.
// We need to skip calling runtime.Callers, this function and the public log function.
const skip = 3
var pcs [1]uintptr
runtime.Callers(skip, pcs[:])
r := slog.NewRecord(time.Now(), level, msg, pcs[0])
r.Add(a...)
if ctx == nil {
ctx = context.Background()
}

_ = l.Handler().Handle(ctx, r)
}

// getCaller returns the program counter of the caller at a given depth.
// The depth is the number of stack frames to ascend, with 0 identifying the
// getCaller function itself, 1 identifying the caller that invoked getCaller,
// and so on.
//
// Example:
//
// pc := getCaller(1) // Returns the program counter of the caller of the function that invoked getCaller.
// pc := getCaller(2) // Returns the program counter of the caller of the function that invoked the function that invoked getCaller.
func getCaller(depth uint8) uintptr {
d := int(depth) + 1

var pcs [1]uintptr
runtime.Callers(d, pcs[:])
return pcs[0]
}
9 changes: 6 additions & 3 deletions internal/logger/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,24 @@ func (l *logger) PanicContext(ctx context.Context, msg string, args ...any) {
panic(msg)
}

// exit is a variable for [os.Exit].
var exit = os.Exit

// Fatal logs at [LevelFatal] and then calls os.Exit(1).
func (l *logger) Fatal(msg string, args ...any) {
l.logAttrs(context.Background(), LevelFatal, msg, args...)
os.Exit(1)
exit(1)
}

// Fatalf logs at LevelFatal and then calls os.Exit(1).
// Arguments are handled in the manner of fmt.Printf.
func (l *logger) Fatalf(msg string, args ...any) {
l.logAttrs(context.Background(), LevelFatal, fmt.Sprintf(msg, args...))
os.Exit(1)
exit(1)
}

// FatalContext logs at [LevelFatal] and then calls os.Exit(1).
func (l *logger) FatalContext(ctx context.Context, msg string, args ...any) {
l.logAttrs(ctx, LevelFatal, msg, args...)
os.Exit(1)
exit(1)
}
63 changes: 62 additions & 1 deletion internal/logger/extensions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func TestLogger_LevelExtensions(t *testing.T) {
}
}

func TestLogger_PanicLevel(t *testing.T) {
func TestLogger_Panic_FatalLevels(t *testing.T) { //nolint:gocyclo // Either higher complexity or code duplication
tests := []struct {
name string
attrs []any
Expand Down Expand Up @@ -220,10 +220,71 @@ func TestLogger_PanicLevel(t *testing.T) {
},
},
},
{
name: "fatal level",
logFunc: func(l Logger, msg string, args ...any) {
l.Fatal(msg, args...)
},
handler: test.MockHandler{
HandleFunc: func(ctx context.Context, r slog.Record) error {
level := LevelFatal
if r.Level != level {
t.Errorf("Expected level to be [%s], got [%s]", getLevelString(level), r.Level)
}
if r.NumAttrs() != 0 {
t.Errorf("Expected 0 attributes, got %d", r.NumAttrs())
}
return nil
},
},
},
{
name: "fatalf level",
logFunc: func(l Logger, msg string, args ...any) {
l.Fatalf(msg, args...)
},
handler: test.MockHandler{
HandleFunc: func(ctx context.Context, r slog.Record) error {
level := LevelFatal
if r.Level != level {
t.Errorf("Expected level to be [%s], got [%s]", getLevelString(level), r.Level)
}
if r.NumAttrs() != 0 {
t.Errorf("Expected 0 attributes, got %d", r.NumAttrs())
}
return nil
},
},
},
{
name: "fatal context level",
logFunc: func(l Logger, msg string, args ...any) {
l.FatalContext(context.Background(), msg, args...)
},
handler: test.MockHandler{
HandleFunc: func(ctx context.Context, r slog.Record) error {
level := LevelFatal
if r.Level != level {
t.Errorf("Expected level to be [%s], got [%s]", getLevelString(level), r.Level)
}
if r.NumAttrs() != 0 {
t.Errorf("Expected 0 attributes, got %d", r.NumAttrs())
}
return nil
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
exit = func(code int) {
if code != 1 {
t.Errorf("Expected exit code 1, got %d", code)
}
panic("os.Exit(1)")
}

l := NewLogger(Options{Handler: tt.handler})
defer func() {
if r := recover(); r == nil {
Expand Down
13 changes: 13 additions & 0 deletions internal/logger/level.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
LevelFatal Level = slog.Level(16)
)

// LevelNames is a map of log levels to their respective names.
var LevelNames = map[Level]string{
LevelTrace: "TRACE",
LevelDebug: "DEBUG",
Expand All @@ -30,6 +31,18 @@ var LevelNames = map[Level]string{
LevelFatal: "FATAL",
}

// LevelColors is a map of log levels to their respective ansi color codes.
var LevelColors = map[Level]string{
LevelTrace: "240", // TRACE - Light Gray
LevelDebug: "63", // DEBUG - Blue
LevelInfo: "86", // INFO - Cyan
LevelNotice: "220", // NOTICE - Yellow
LevelWarn: "192", // WARN - Orange
LevelError: "204", // ERROR - Red
LevelPanic: "134", // PANIC - Purple
LevelFatal: "160", // FATAL - Dark Red
}

// getLevel returns the integer value of the given level string.
// If the level is not recognized, it returns LevelInfo.
func getLevel(level string) int {
Expand Down
4 changes: 2 additions & 2 deletions internal/logger/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ func newDefaultOptions() Options {
}
}

// getOptions returns the first Options in the slice if it exists; otherwise, it returns the default Options.
func getOptions(o ...Options) Options {
// newOptions creates a new Options instance with the provided Options merged with the default Options.
func newOptions(o ...Options) Options {
opts := newDefaultOptions()
if len(o) > 0 {
return o[0].merge(opts)
Expand Down
Loading

0 comments on commit a544882

Please sign in to comment.