diff --git a/llog/default_test.go b/llog/default_test.go new file mode 100644 index 0000000..3ced467 --- /dev/null +++ b/llog/default_test.go @@ -0,0 +1,20 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package llog_test + +import ( + "log/slog" + + "github.com/u-root/uio/llog" +) + +func ExampleDefault_withtime() { + l := llog.Default() + l.Infof("An INFO level string") + l.Debugf("A DEBUG level that does not appear") + + l.Level = slog.LevelDebug + l.Debugf("A DEBUG level that appears") +} diff --git a/llog/example_test.go b/llog/example_test.go new file mode 100644 index 0000000..bdb809c --- /dev/null +++ b/llog/example_test.go @@ -0,0 +1,27 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package llog_test + +import ( + "flag" + "log/slog" + + "github.com/u-root/uio/llog" +) + +func someFunc(v llog.Printf) { + v("logs at the given level %s", "foo") +} + +func Example() { + l := llog.Default() + // If -v is set, l.Level becomes slog.LevelDebug. + l.RegisterDebugFlag(flag.CommandLine, "v") + flag.Parse() + + someFunc(l.Debugf) + someFunc(l.AtLevelFunc(slog.LevelWarn)) + someFunc(l.Infof) +} diff --git a/llog/levellog.go b/llog/levellog.go new file mode 100644 index 0000000..5da92a5 --- /dev/null +++ b/llog/levellog.go @@ -0,0 +1,168 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package llog is a dirt-simple leveled text logger. +package llog + +import ( + "flag" + "fmt" + "log" + "log/slog" + "math" + "strconv" + "testing" +) + +// Default is the stdlib default log sink. +func Default() *Logger { + return &Logger{Sink: SinkFor(log.Printf)} +} + +// Test is a logger that prints every level to t.Logf. +func Test(tb testing.TB) *Logger { + tb.Helper() + return &Logger{Sink: SinkFor(tb.Logf), Level: math.MinInt32} +} + +// Debug prints to log.Printf at the debug level. +func Debug() *Logger { + return &Logger{Sink: SinkFor(log.Printf), Level: slog.LevelDebug} +} + +// Printf is a logger printf function. +type Printf func(format string, v ...any) + +// Printfer is an interface implementing Printf. +type Printfer interface { + Printf(format string, v ...any) +} + +// Sink is the output for Logger. +type Sink func(level slog.Level, format string, v ...any) + +// SinkFor prepends the log with a log level and outputs to p. +func SinkFor(p Printf) Sink { + return func(level slog.Level, format string, args ...any) { + // Prepend log level. + format = "%s " + format + args = append([]any{level}, args...) + p(format, args...) + } +} + +// Logger is a dirt-simple leveled logger. +// +// If the log level is >= Level, it logs to the given Sink. +// +// Logger or Sink may be nil in order to log nothing. +type Logger struct { + Sink Sink + Level slog.Level +} + +// New creates a logger from p which prepends the log level to the output and +// uses l as the default log level. +// +// Logs with level >= l will be printed using p. +func New(l slog.Level, p Printf) *Logger { + return &Logger{ + Sink: SinkFor(p), + Level: l, + } +} + +// RegisterLevelFlag registers a flag that sets the given numeric level as the level. +func (l *Logger) RegisterLevelFlag(f *flag.FlagSet, flagName string) { + f.IntVar((*int)(&l.Level), flagName, int(l.Level), "Level to log at. Lower level emits more logs. -4 = DEBUG, 0 = INFO, 4 = WARN, 8 = ERROR") +} + +// RegisterVerboseFlag registers a boolean flag that, if set, assigns verboseLevel as the level. +func (l *Logger) RegisterVerboseFlag(f *flag.FlagSet, flagName string, verboseLevel slog.Level) { + f.BoolFunc(flagName, fmt.Sprintf("If set, logs at %d level", verboseLevel), func(val string) error { + b, err := strconv.ParseBool(val) + if err != nil { + return err + } + if b { + l.Level = verboseLevel + } + return nil + }) +} + +// RegisterDebugFlag registers a boolean flag that, if set, assigns LevelDebug as the level. +func (l *Logger) RegisterDebugFlag(f *flag.FlagSet, flagName string) { + l.RegisterVerboseFlag(f, flagName, slog.LevelDebug) +} + +// AtLevelFunc returns a Printf that can be passed around to log at the given level. +// +// AtLevelFunc never returns nil. +func (l *Logger) AtLevelFunc(level slog.Level) Printf { + if l == nil || l.Sink == nil { + return func(fmt string, args ...any) {} + } + return func(fmt string, args ...any) { + l.Logf(level, fmt, args...) + } +} + +type printfer struct { + printf Printf +} + +// Printf implements Printfer. +func (p printfer) Printf(format string, v ...any) { + p.printf(format, v...) +} + +// AtLevel returns a Printfer that can be passed around to log at the given level. +// +// AtLevel never returns nil. +func (l *Logger) AtLevel(level slog.Level) Printfer { + return printfer{printf: l.AtLevelFunc(level)} +} + +// Debugf is a printf function that logs at the Debug level. +func (l *Logger) Debugf(fmt string, args ...any) { + if l == nil { + return + } + l.Logf(slog.LevelDebug, fmt, args...) +} + +// Infof is a printf function that logs at the Info level. +func (l *Logger) Infof(fmt string, args ...any) { + if l == nil { + return + } + l.Logf(slog.LevelInfo, fmt, args...) +} + +// Warnf is a printf function that logs at the Warn level. +func (l *Logger) Warnf(fmt string, args ...any) { + if l == nil { + return + } + l.Logf(slog.LevelWarn, fmt, args...) +} + +// Errorf is a printf function that logs at the Error level. +func (l *Logger) Errorf(fmt string, args ...any) { + if l == nil { + return + } + l.Logf(slog.LevelError, fmt, args...) +} + +// Logf logs at the given level. +func (l *Logger) Logf(level slog.Level, fmt string, args ...any) { + if l == nil || l.Sink == nil { + return + } + if level >= l.Level { + l.Sink(level, fmt, args...) + } +} diff --git a/llog/llog_test.go b/llog/llog_test.go new file mode 100644 index 0000000..6d19990 --- /dev/null +++ b/llog/llog_test.go @@ -0,0 +1,185 @@ +// Copyright 2024 the u-root Authors. All rights reserved +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package llog + +import ( + "flag" + "fmt" + "log" + "log/slog" + "strconv" + "strings" + "testing" +) + +func TestLevelFlag(t *testing.T) { + for _, tt := range []struct { + args []string + want slog.Level + }{ + { + args: []string{"-level=4"}, + want: slog.LevelWarn, + }, + { + args: []string{}, + want: slog.LevelInfo, + }, + } { + f := flag.NewFlagSet("", flag.ContinueOnError) + + v := &Logger{} + v.RegisterLevelFlag(f, "level") + _ = f.Parse(tt.args) + + if v.Level != tt.want { + t.Errorf("Parse(%#v) = %v, want %v", tt.args, v, tt.want) + } + } + + for _, tt := range []struct { + args []string + want slog.Level + err error + }{ + { + args: []string{"-v"}, + want: slog.LevelWarn, + }, + { + args: []string{}, + want: slog.LevelInfo, + }, + { + args: []string{"-v=true"}, + want: slog.LevelWarn, + }, + { + args: []string{"-v=true", "-v=false"}, + want: slog.LevelWarn, + }, + { + args: []string{"-v=foobar"}, + want: slog.LevelInfo, + err: strconv.ErrSyntax, + }, + } { + f := flag.NewFlagSet("", flag.ContinueOnError) + + v := &Logger{} + v.RegisterVerboseFlag(f, "v", slog.LevelWarn) + // Parse doesn't use %w. + if err := f.Parse(tt.args); err != tt.err && err != nil && !strings.Contains(err.Error(), tt.err.Error()) { + t.Errorf("Parse(%#v) = %v, want %v", tt.args, err, tt.err) + } + if v.Level != tt.want { + t.Errorf("Parse(%#v) = %v, want %v", tt.args, v, tt.want) + } + } + + for _, tt := range []struct { + args []string + want slog.Level + err error + }{ + { + args: []string{"-v"}, + want: slog.LevelDebug, + }, + { + args: []string{}, + want: slog.LevelInfo, + }, + { + args: []string{"-v=true"}, + want: slog.LevelDebug, + }, + { + args: []string{"-v=true", "-v=false"}, + want: slog.LevelDebug, + }, + { + args: []string{"-v=foobar"}, + want: slog.LevelInfo, + err: strconv.ErrSyntax, + }, + } { + f := flag.NewFlagSet("", flag.ContinueOnError) + + v := &Logger{} + v.RegisterDebugFlag(f, "v") + // Parse doesn't use %w. + if err := f.Parse(tt.args); err != tt.err && err != nil && !strings.Contains(err.Error(), tt.err.Error()) { + t.Errorf("Parse(%#v) = %v, want %v", tt.args, err, tt.err) + } + if v.Level != tt.want { + t.Errorf("Parse(%#v) = %v, want %v", tt.args, v, tt.want) + } + } +} + +func TestNilLogger(t *testing.T) { + for _, l := range []*Logger{nil, {}} { + // Test that none of this panics. + l.AtLevelFunc(slog.LevelDebug)("nothing") + l.AtLevel(slog.LevelDebug).Printf("nothing") + l.Debugf("nothing") + l.Infof("nothing") + l.Warnf("nothing") + l.Errorf("nothing") + l.Logf(slog.LevelDebug, "nothing") + } +} + +func TestLog(t *testing.T) { + var s strings.Builder + l := New(slog.LevelDebug, func(format string, args ...any) { + fmt.Fprintf(&s, format+"\n", args...) + }) + + l.AtLevelFunc(slog.LevelDebug)("nothing") + l.AtLevel(slog.LevelInfo).Printf("nothing") + l.Debugf("nothing") + l.Infof("nothing") + l.Warnf("nothing") + l.Errorf("nothing") + l.Logf(slog.LevelDebug, "nothing") + + want := `DEBUG nothing +INFO nothing +DEBUG nothing +INFO nothing +WARN nothing +ERROR nothing +DEBUG nothing +` + if got := s.String(); got != want { + t.Errorf("got %v, want %v", got, want) + } +} + +func TestDefaults(t *testing.T) { + var s strings.Builder + log.SetOutput(&s) + log.SetFlags(0) + + l := Debug() + l.Debugf("foobar") + want := "DEBUG foobar\n" + if got := s.String(); got != want { + t.Errorf("got %v, want %v", got, want) + } + + l = Default() + l.Debugf("bazzed") + l.Infof("stuff") + want = "DEBUG foobar\nINFO stuff\n" + if got := s.String(); got != want { + t.Errorf("got %v, want %v", got, want) + } + + l = Test(t) + l.Debugf("more foobar") +}