diff --git a/bypass.go b/bypass.go new file mode 100644 index 0000000..8f5695e --- /dev/null +++ b/bypass.go @@ -0,0 +1,72 @@ +package zwrap + +func (l *Logger) checkPanicBypass(fmt string, v ...interface{}) (string, []interface{}, bool) { + if !l.noPanic { + return fmt, v, true + } + + if fmt != "" { + fmt = "[PANIC BYPASSED] " + fmt + return fmt, v, false + } + + nv := make([]interface{}, len(v)+1) + nv[0] = "[PANIC BYPASSED]" + copy(nv[1:], v) + v = nil + return fmt, nv, false +} + +func (l *Logger) checkFatalBypass(fmt string, v ...interface{}) (string, []interface{}, bool) { + if !l.noFatal { + return fmt, v, true + } + + if fmt != "" { + fmt = "[FATAL BYPASSED] " + fmt + return fmt, v, false + } + + nv := make([]interface{}, len(v)+1) + nv[0] = "[FATAL BYPASSED]" + copy(nv[1:], v) + v = nil + return fmt, nv, false +} + +func (l *Logger) NoPanics(b bool) { + l.mu.Lock() + l.noPanic = b + l.mu.Unlock() +} + +func (l *Logger) NoFatals(b bool) { + l.mu.Lock() + l.noFatal = b + l.mu.Unlock() +} + +func (l *Logger) WithNoPanics() *Logger { + l.NoPanics(true) + return l +} + +func (l *Logger) WithNoFatals() *Logger { + l.NoFatals(true) + return l +} + +func (l *Logger) ForceLevel(level any) { + l.mu.Lock() + nl := castToZlogLevel(level) + l.forceLevel = &nl + l.printLevel = nl + nll := l.Logger.Level(nl) + l.Logger = &nll + l.mu.Unlock() +} + +func (l *Logger) WithForceLevel(level uint32) *Logger { + l.ForceLevel(level) + return l +} diff --git a/go.mod b/go.mod index 82f47bf..34875ba 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module git.tcp.direct/kayos/zwrap -go 1.18 +go 1.20 -require github.com/rs/zerolog v1.30.0 +require github.com/rs/zerolog v1.32.0 require ( - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - golang.org/x/sys v0.18.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + golang.org/x/sys v0.20.0 // indirect ) diff --git a/go.sum b/go.sum index b05abda..725b1de 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,16 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= -github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/level.go b/level.go new file mode 100644 index 0000000..268c7c2 --- /dev/null +++ b/level.go @@ -0,0 +1,84 @@ +package zwrap + +import ( + "fmt" + + "github.com/rs/zerolog" +) + +type Level interface { + int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64 +} + +func castToZlogLevel(level any) zerolog.Level { + switch casted := level.(type) { + case int: + return toZlogLevel[int](casted) + case int8: + return toZlogLevel[int8](casted) + case int16: + return toZlogLevel[int16](casted) + case int32: + return toZlogLevel[int32](casted) + case int64: + return toZlogLevel[int64](casted) + case uint: + return toZlogLevel[uint](casted) + case uint8: + return toZlogLevel[uint8](casted) + case uint16: + return toZlogLevel[uint16](casted) + case uint32: + return toZlogLevel[uint32](casted) + case uint64: + return toZlogLevel[uint64](casted) + case string: + if parsed, err := zerolog.ParseLevel(casted); err == nil { + return parsed + } else { + panic(fmt.Sprintf("invalid log level string %v: %v", level, err)) + } + case zerolog.Level: + return casted + default: + panic(fmt.Sprintf("invalid log level type (%T): %v", level, level)) + } +} + +func toZlogLevel[T Level](level T) zerolog.Level { + switch casted := any(level).(type) { + case uint32: // compat + switch { + case casted == 0: + return zerolog.PanicLevel + case casted == 1: + return zerolog.FatalLevel + case casted == 2: + return zerolog.ErrorLevel + case casted == 3: + return zerolog.WarnLevel + case casted == 4: + return zerolog.InfoLevel + case casted == 5: + return zerolog.DebugLevel + case casted == 6: + return zerolog.TraceLevel + default: + if casted < 0 { + return zerolog.TraceLevel + } + if casted > 5 { + return zerolog.PanicLevel + } + } + case int16, int32, int64, int, uint, uint8, uint16, uint64, int8: + if level < 0 { + return zerolog.TraceLevel + } + if level > 5 { + return zerolog.PanicLevel + } + return zerolog.Level(int8(level)) + } + return zerolog.TraceLevel +} diff --git a/level_test.go b/level_test.go new file mode 100644 index 0000000..3d98934 --- /dev/null +++ b/level_test.go @@ -0,0 +1,108 @@ +package zwrap + +import ( + "testing" + + "github.com/rs/zerolog" +) + +func TestCastToZlogLevel(t *testing.T) { + tests := []struct { + name string + level any + want zerolog.Level + }{ + {"int", 3, zerolog.ErrorLevel}, + {"int8", int8(4), zerolog.FatalLevel}, + {"int16", int16(2), zerolog.WarnLevel}, + {"int32", int32(1), zerolog.InfoLevel}, + {"int64", int64(5), zerolog.PanicLevel}, + {"uint", uint(6), zerolog.PanicLevel}, + {"uint8", uint8(0), zerolog.DebugLevel}, + {"uint16", uint16(4), zerolog.FatalLevel}, + {"uint32", uint32(3), zerolog.WarnLevel}, + {"uint64", uint64(2), zerolog.WarnLevel}, + {"string", "info", zerolog.InfoLevel}, + {"zerolog.Level", zerolog.DebugLevel, zerolog.DebugLevel}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := castToZlogLevel(tt.level); got != tt.want { + t.Errorf("castToZlogLevel() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCastToZlogLevel_Panic(t *testing.T) { + tests := []struct { + name string + level any + }{ + {"invalid type", 3.14}, + {"invalid string", "invalid"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("castToZlogLevel() did not panic") + } + }() + castToZlogLevel(tt.level) + }) + } +} + +func TestToZlogLevel(t *testing.T) { + tests := []struct { + name string + level any + want zerolog.Level + }{ + {"int8", int8(3), zerolog.ErrorLevel}, + {"int16", int16(2), zerolog.WarnLevel}, + {"int32", int32(1), zerolog.InfoLevel}, + {"int64", int64(5), zerolog.PanicLevel}, + {"uint", uint(0), zerolog.DebugLevel}, + {"uint8", uint8(6), zerolog.PanicLevel}, + {"uint16", uint16(4), zerolog.FatalLevel}, + {"uint32", uint32(3), zerolog.WarnLevel}, + {"uint64", uint64(2), zerolog.WarnLevel}, + {"-1 trace level", -1, zerolog.TraceLevel}, + {"> limit level", uint(7), zerolog.PanicLevel}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + switch casted := tt.level.(type) { + case int8: + if got := toZlogLevel[int8](casted); got != tt.want { + t.Errorf("toZlogLevel(%v) = %v, want %v", tt.level, got, tt.want) + } + case int16: + if got := toZlogLevel[int16](casted); got != tt.want { + t.Errorf("toZlogLevel(%v) = %v, want %v", tt.level, got, tt.want) + } + case int32: + if got := toZlogLevel[int32](casted); got != tt.want { + t.Errorf("toZlogLevel(%v) = %v, want %v", tt.level, got, tt.want) + } + case int64: + if got := toZlogLevel[int64](casted); got != tt.want { + t.Errorf("toZlogLevel(%v) = %v, want %v", tt.level, got, tt.want) + } + case uint: + if got := toZlogLevel[uint](casted); got != tt.want { + t.Errorf("toZlogLevel(%v) = %v, want %v", tt.level, got, tt.want) + } + case uint8: + if got := toZlogLevel[uint8](casted); got != tt.want { + t.Errorf("toZlogLevel(%v) = %v, want %v", tt.level, got, tt.want) + } + } + }) + } +} diff --git a/restore_colors_test.go b/restore_colors_test.go new file mode 100644 index 0000000..22bf006 --- /dev/null +++ b/restore_colors_test.go @@ -0,0 +1,43 @@ +package zwrap + +import ( + "bytes" + "testing" + + "github.com/rs/zerolog" +) + +type colorTester struct { + t *testing.T + last []byte +} + +func (c *colorTester) Write(p []byte) (n int, err error) { + if len(c.last) == 0 { + c.last = make([]byte, len(p)) + copy(c.last, p) + return len(p), nil + } + if bytes.Equal(c.last, p) { + c.t.Errorf("\ncaught second output: %s\nwhich is the same as the first: %s", string(p), string(c.last)) + } + return len(p), nil +} + +func TestLegacyColorizer(t *testing.T) { + tw := &colorTester{t: t} + zlc := zerolog.NewConsoleWriter() + zlc.Out = tw + zlc.NoColor = false + zl := Wrap(zerolog.New(zlc)) + zl.Trace("yeet") + if len(tw.last) == 0 { + t.Fatalf("test writer busted") + } + zlc = zerolog.NewConsoleWriter() + zlc.FormatLevel = LogLevelFmt(false) + zlc.NoColor = false + zlc.Out = tw + zl = Wrap(zerolog.New(zlc)) + zl.Trace("yeet") +} diff --git a/wrap.go b/wrap.go index e8a7fd8..88cbe73 100644 --- a/wrap.go +++ b/wrap.go @@ -13,18 +13,21 @@ import ( type Logger struct { *zerolog.Logger - *sync.RWMutex + mu *sync.RWMutex prefix string printLevel zerolog.Level + forceLevel *zerolog.Level + noPanic bool + noFatal bool } func (l *Logger) Warning(args ...any) { - l.Logger.Warn().Msg(fmt.Sprint(args...)) + l.printLn(l.Logger.Warn(), false, args...) } func (l *Logger) Warningln(args ...any) { - l.Logger.Warn().Msg(fmt.Sprintln(args...)) + l.printLn(l.Logger.Warn(), false, args...) } func (l *Logger) V(level int) bool { @@ -38,176 +41,226 @@ func (l *Logger) V(level int) bool { } func (l *Logger) SetPrefix(prefix string) { - l.Lock() + l.mu.Lock() l.prefix = prefix - l.Unlock() + l.mu.Unlock() } func (l *Logger) SetPrintLevel(level zerolog.Level) { - l.Lock() + l.mu.Lock() l.printLevel = level - l.Unlock() + l.mu.Unlock() } func (l *Logger) Prefix() string { - l.RLock() - defer l.RUnlock() + l.mu.RLock() + p := l.myPrefix() + l.mu.RUnlock() + return p +} + +func (l *Logger) myPrefix() string { return l.prefix } func (l *Logger) Println(v ...interface{}) { - l.RLock() - l.Logger.WithLevel(l.printLevel).Msg(fmt.Sprint(v...)) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.WithLevel(l.printLevel), false, v...) + l.mu.RUnlock() } func (l *Logger) Printf(format string, v ...interface{}) { - l.RLock() - l.Logger.WithLevel(l.printLevel).Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + var str string + switch { + case len(v) == 0: + str = format + case len(v) == 1: + str = fmt.Sprintf(format, v[0]) + default: + str = fmt.Sprintf(format, v...) + } + l.printLn(l.Logger.WithLevel(l.printLevel), false, str) + l.mu.RUnlock() } func (l *Logger) Print(v ...interface{}) { - l.RLock() - l.Logger.WithLevel(l.printLevel).Msg(fmt.Sprint(v...)) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.WithLevel(l.printLevel), false, v...) + l.mu.RUnlock() } func (l *Logger) Fatal(v ...interface{}) { - // Don't check mutex here because we're exiting anyway. - printLn(l.Logger.Fatal(), v...) + var ok bool + if _, v, ok = l.checkFatalBypass("", v...); ok { + l.printLn(l.Logger.Fatal(), true, v...) + return + } + l.mu.RLock() + l.printLn(l.Logger.Error(), false, v...) + l.mu.RUnlock() } func (l *Logger) Fatalf(format string, v ...interface{}) { - // Don't check mutex here because we're exiting anyway. - l.Logger.Fatal().Msgf(format, v...) + var ok bool + if format, v, ok = l.checkFatalBypass(format, v...); ok { + l.printLn(l.Logger.Fatal(), true, fmt.Sprintf(format, v...)) + return + } + l.mu.RLock() + l.printLn(l.Logger.Error(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Fatalln(v ...interface{}) { - // Don't check mutex here because we're exiting anyway. - printLn(l.Logger.Fatal(), v...) + var ok bool + if _, v, ok = l.checkFatalBypass("", v...); ok { + l.printLn(l.Logger.Fatal(), true, v...) + return + } + l.mu.RLock() + l.printLn(l.Logger.Error(), false, v...) + l.mu.RUnlock() } func (l *Logger) Panic(v ...interface{}) { - // Don't check mutex here because we're panicking anyway. - printLn(l.Logger.Panic(), v...) + var ok bool + if _, v, ok = l.checkPanicBypass("", v...); ok { + l.printLn(l.Logger.Panic(), true, v...) + return + } + l.mu.RLock() + l.printLn(l.Logger.Error(), false, v...) + l.mu.RUnlock() } func (l *Logger) Panicf(format string, v ...interface{}) { - // Don't check mutex here because we're panicking anyway. - l.Logger.Panic().Msgf(format, v...) + var ok bool + if format, v, ok = l.checkPanicBypass(format, v...); ok { + l.printLn(l.Logger.Panic(), true, fmt.Sprintf(format, v...)) + return + } + l.mu.RLock() + l.printLn(l.Logger.Error(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Panicln(v ...interface{}) { - // Don't check mutex here because we're panicking anyway. - printLn(l.Logger.Panic(), v...) + var ok bool + if _, v, ok = l.checkPanicBypass("", v...); ok { + l.printLn(l.Logger.Panic(), true, v...) + return + } + l.mu.RLock() + l.printLn(l.Logger.Error(), false, v...) + l.mu.RUnlock() } func (l *Logger) Errorf(format string, v ...interface{}) { - l.RLock() - l.Logger.Error().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Error(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Warnf(format string, v ...interface{}) { - l.RLock() - l.Logger.Warn().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Warn(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Infof(format string, v ...interface{}) { - l.RLock() - l.Logger.Info().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Info(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Debugf(format string, v ...interface{}) { - l.RLock() - l.Logger.Debug().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Debug(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Tracef(format string, v ...interface{}) { - l.RLock() - l.Logger.Trace().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Trace(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Error(v ...interface{}) { - l.RLock() - printLn(l.Logger.Error(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Error(), false, v...) + l.mu.RUnlock() } func (l *Logger) Warn(v ...interface{}) { - l.RLock() - printLn(l.Logger.Warn(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Warn(), false, v...) + l.mu.RUnlock() } func (l *Logger) Info(v ...interface{}) { - l.RLock() - printLn(l.Logger.Info(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Info(), false, v...) + l.mu.RUnlock() } func (l *Logger) Debug(v ...interface{}) { - l.RLock() - printLn(l.Logger.Debug(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Debug(), false, v...) + l.mu.RUnlock() } func (l *Logger) Trace(v ...interface{}) { - l.RLock() - printLn(l.Logger.Trace(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Trace(), false, v...) + l.mu.RUnlock() } func (l *Logger) Errorln(v ...interface{}) { - l.RLock() - printLn(l.Logger.Error(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Error(), false, v...) + l.mu.RUnlock() } func (l *Logger) Warnln(v ...interface{}) { - l.RLock() - printLn(l.Logger.Warn(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Warn(), false, v...) + l.mu.RUnlock() } func (l *Logger) Infoln(v ...interface{}) { - l.RLock() - printLn(l.Logger.Info(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Info(), false, v...) + l.mu.RUnlock() } func (l *Logger) Debugln(v ...interface{}) { - l.RLock() - printLn(l.Logger.Debug(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Debug(), false, v...) + l.mu.RUnlock() } func (l *Logger) Traceln(v ...interface{}) { - l.RLock() - printLn(l.Logger.Trace(), v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Trace(), false, v...) + l.mu.RUnlock() } func (l *Logger) Verbosef(format string, v ...interface{}) { - l.RLock() - l.Logger.Trace().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Trace(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Noticef(format string, v ...interface{}) { - l.RLock() - l.Logger.Info().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Info(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) Warningf(format string, v ...interface{}) { - l.RLock() - l.Logger.Warn().Msgf(format, v...) - l.RUnlock() + l.mu.RLock() + l.printLn(l.Logger.Warn(), false, fmt.Sprintf(format, v...)) + l.mu.RUnlock() } func (l *Logger) WithPrefix(prefix string) *Logger { @@ -220,50 +273,30 @@ func (l *Logger) Logf(format string, v ...interface{}) { } func (l *Logger) WithFields(fields map[string]interface{}) *Logger { - l.RLock() + l.mu.RLock() nl := l.Logger.With().Fields(fields).Logger() l.Logger = &nl - l.RUnlock() + l.mu.RUnlock() return l } // SetLevel is compatibility for ghettovoice/gosip/log.Logger -func (l *Logger) SetLevel(level uint32) { - l.Lock() - nl := l.Logger.Level(gosipLevelToZerologLevel(level)) +func (l *Logger) SetLevel(level any) { + l.mu.Lock() + nl := l.Logger.Level(castToZlogLevel(level)) l.Logger = &nl - l.Unlock() -} - -func gosipLevelToZerologLevel(level uint32) zerolog.Level { - switch level { - case 0: - return zerolog.PanicLevel - case 1: - return zerolog.FatalLevel - case 2: - return zerolog.ErrorLevel - case 3: - return zerolog.WarnLevel - case 4: - return zerolog.InfoLevel - case 5: - return zerolog.DebugLevel - case 6: - return zerolog.TraceLevel - } - panic(fmt.Sprintf("invalid log level %d", level)) + l.mu.Unlock() } func (l *Logger) Write(p []byte) (n int, err error) { - l.RLock() + l.mu.RLock() l.Logger.WithLevel(l.printLevel).Msg(string(bytes.TrimSuffix(p, []byte("\n")))) - l.RUnlock() + l.mu.RUnlock() return len(p), nil } func (l *Logger) Output(calldepth int, s string) error { - l.RLock() + l.mu.RLock() event := l.Logger.Info() if calldepth != 2 { if l.prefix != "" { @@ -274,11 +307,48 @@ func (l *Logger) Output(calldepth int, s string) error { } event.Msg(s) zerolog.CallerFieldName = "caller" - l.RUnlock() + l.mu.RUnlock() return nil } -func printLn(e *zerolog.Event, v ...interface{}) { +func (l *Logger) transformZEvent(e *zerolog.Event) *zerolog.Event { + switch *l.forceLevel { + case zerolog.PanicLevel: + if !l.noPanic { + e = l.Logger.Panic() + } + case zerolog.FatalLevel: + if !l.noFatal { + e = l.Logger.Fatal() + } + case zerolog.ErrorLevel: + e = l.Logger.Error() + case zerolog.WarnLevel: + e = l.Logger.Warn() + case zerolog.InfoLevel: + e = l.Logger.Info() + case zerolog.DebugLevel: + e = l.Logger.Debug() + case zerolog.TraceLevel: + e = l.Logger.Trace() + default: + panic(fmt.Sprintf("invalid logger config, bad force level %v", l.forceLevel)) + } + return e +} + +func (l *Logger) printLn(e *zerolog.Event, preserve bool, v ...interface{}) { + if l.forceLevel != nil && !preserve { + e = l.transformZEvent(e) + } + if len(v) == 0 { + e.Msg("") + return + } + if len(v) == 1 { + e.Msg(fmt.Sprint(v[0])) + return + } strBuf := strBufs.Get().(*strings.Builder) for i, val := range v { if i > 0 { @@ -292,18 +362,18 @@ func printLn(e *zerolog.Event, v ...interface{}) { } type prefixHook struct { - parent StdCompatLogger + parent *Logger } func (h prefixHook) Run(e *zerolog.Event, _ zerolog.Level, _ string) { if h.parent.Prefix() != "" { - e.Str("caller", h.parent.Prefix()) + e.Str("caller", h.parent.myPrefix()) } } func Wrap(l zerolog.Logger) *Logger { wrapped := &Logger{ - RWMutex: &sync.RWMutex{}, + mu: &sync.RWMutex{}, printLevel: zerolog.InfoLevel, } p := prefixHook{wrapped} diff --git a/wrap_test.go b/wrap_test.go index c540011..8df2269 100644 --- a/wrap_test.go +++ b/wrap_test.go @@ -14,6 +14,7 @@ type testWriter struct { t *testing.T needsPrefix string mustNotHavePrefix string + mustLevel *zerolog.Level } var ErrPrefixMismatch = errors.New("prefix mismatch") @@ -29,6 +30,20 @@ func (w *testWriter) Write(p []byte) (n int, err error) { w.t.Errorf("unexpected prefix %q, got %q", w.mustNotHavePrefix, line) return 0, ErrPrefixMismatch } + + if w.mustLevel != nil { + if !strings.Contains(line, w.mustLevel.String()) { + w.t.Errorf("expected level %q, got %q", w.mustLevel.String(), line) + return 0, ErrPrefixMismatch + } + lvl := strings.Split(line, `"level":"`)[1] + lvl = strings.Split(lvl, `"`)[0] + if lvl != w.mustLevel.String() { + w.t.Errorf("expected level %q, got %q", w.mustLevel.String(), lvl) + return 0, ErrPrefixMismatch + } + } + w.t.Log(line) return len(p), nil } @@ -46,7 +61,44 @@ func (n *needsLogger) SetLogger(logger aLogger) { } func (n *needsLogger) DoSomething() { - n.logger.Println("Hello, world!") + n.logger.Println("yeet") +} + +type leveled struct { + name string + test func(*Logger, *testing.T) + shouldPanic bool + panicked bool + t *testing.T +} + +const ( + expected = "expected" + unexpected = "unexpected" +) + +func (l *leveled) expS() string { + if l.shouldPanic { + return expected + } + return unexpected +} + +func (l *leveled) Run() { + l.t.Run(l.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + l.t.Logf("caught panic (%s): %v", l.expS(), r) + l.panicked = true + } + }() + zl := zerolog.New(os.Stderr).With().Timestamp().Logger() + wrapped := Wrap(zl) + l.test(wrapped, t) + }) + if (l.shouldPanic && !l.panicked) || (!l.shouldPanic && l.panicked) { + l.t.Errorf("%s panic during test: %s", l.expS(), l.name) + } } func TestWrap(t *testing.T) { @@ -55,56 +107,209 @@ func TestWrap(t *testing.T) { writah := &testWriter{t: t} zl := zerolog.New(writah).With().Timestamp().Logger() - wrapped := Wrap(zl) + wzl := Wrap(zl) myThing := &needsLogger{} - myThing.SetLogger(wrapped) + myThing.SetLogger(wzl) - multiLog := func(v ...interface{}) { + multiLog := func(wrapped *Logger, v ...interface{}) { t.Helper() + toggleFatals := wrapped.noFatal == false + togglePanics := wrapped.noPanic == false wrapped.Print(v...) - wrapped.Printf("%v", v) + wrapped.Printf("f: %v", v...) wrapped.Println(v...) wrapped.Error(v...) - wrapped.Errorf("%v", v) + wrapped.Errorf("f: %v", v...) wrapped.Errorln(v...) wrapped.Debug(v...) - wrapped.Debugf("%v", v) + wrapped.Debugf("f: %v", v...) wrapped.Debugln(v...) wrapped.Warn(v...) - wrapped.Warnf("%v", v) + wrapped.Warnf("f: %v", v...) wrapped.Warnln(v...) wrapped.Info(v...) - wrapped.Infof("%v", v) + wrapped.Infof("f: %v", v...) wrapped.Infoln(v...) - wrapped.Tracef("%v", v) + wrapped.Tracef("f: %v", v...) wrapped.Trace(v...) wrapped.Traceln(v...) - wrapped.Logf("%v", v) - wrapped.Warning("%v", v) - wrapped.Warningf("%v", v) + wrapped.Logf("f: %v", v...) + wrapped.Warning(v...) + wrapped.Warningf("f: %v", v...) + wrapped.Println("") + wrapped.Println() + if toggleFatals { + wrapped.NoFatals(true) + } + wrapped.Fatal(v...) + wrapped.Fatalf("f: %v", v...) + wrapped.Fatalln(v...) + if toggleFatals { + wrapped.NoFatals(false) + } + if togglePanics { + wrapped.NoPanics(true) + } + wrapped.Panic(v...) + wrapped.Panicf("f: %v", v...) + wrapped.Panicln(v...) + if togglePanics { + wrapped.NoPanics(false) + } } - if wrapped.V(0) { + if wzl.V(0) { t.Error("V(0) should always return false") } t.Run("generic", func(t *testing.T) { - multiLog("Hello, world!") + multiLog(wzl, "yeet") }) t.Run("prefix", func(t *testing.T) { writah.needsPrefix = "prefix: " - wrapped.SetPrefix("prefix: ") - multiLog("Hello, world!") + wzl.SetPrefix("prefix: ") + multiLog(wzl, "yeet") + writah.needsPrefix = "prefix2: " + wzl = wzl.WithPrefix("prefix2: ") + multiLog(wzl, "yeet") }) t.Run("remove prefix", func(t *testing.T) { writah.needsPrefix = "" writah.mustNotHavePrefix = "prefix: " - wrapped.SetPrefix("") - multiLog("Hello, world!") + wzl.SetPrefix("") + multiLog(wzl, "yeet") }) + forceLevelTests := []leveled{ + { + name: "trace", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.TraceLevel) + multiLog(wrapped, "yeet") + }, + shouldPanic: false, + t: t, + }, + { + name: "trace_with_panic", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.TraceLevel) + multiLog(wrapped, "yeet") + wrapped.Panic("yeet") + }, + shouldPanic: true, + t: t, + }, + { + name: "debug", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.DebugLevel) + multiLog(wrapped, "yeet") + }, + shouldPanic: false, + t: t, + }, + { + name: "info", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.InfoLevel) + multiLog(wrapped, "yeet") + }, + shouldPanic: false, + t: t, + }, + { + name: "warn", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.WarnLevel) + multiLog(wrapped, "yeet") + }, + shouldPanic: false, + t: t, + }, + { + name: "error", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.ErrorLevel) + multiLog(wrapped, "yeet") + }, + shouldPanic: false, + t: t, + }, + { + name: "fatal", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.FatalLevel) + wrapped.NoFatals(true) + multiLog(wrapped, "yeet") + }, + shouldPanic: false, + t: t, + }, + { + name: "panic", + test: func(wrapped *Logger, t *testing.T) { + wrapped.ForceLevel(zerolog.PanicLevel) + multiLog(wrapped, "yeet") + }, + shouldPanic: true, + t: t, + }, + { + name: "panic_with_panic", + test: func(wrapped *Logger, t *testing.T) { + wrapped.Panic("yeet") + }, + shouldPanic: true, + t: t, + }, + } + + for _, test := range forceLevelTests { + test.name = "force_level_" + test.name + test.Run() + } + + panicAndFatalBypassTests := []leveled{ + { + name: "no_panic", + test: func(wrapped *Logger, t *testing.T) { + wrapped.NoPanics(true) + wrapped.Panic("yeet!!") + }, + shouldPanic: false, + t: t, + }, + { + name: "no_fatal", + test: func(wrapped *Logger, t *testing.T) { + wrapped.NoFatals(true) + wrapped.Fatal("yeet") + }, + shouldPanic: false, // I guess the test should fail if it os.Exits anyway..? :^) + t: t, + }, + { + name: "no_panic_no_fatal_force_panic", + test: func(wrapped *Logger, t *testing.T) { + wrapped.NoPanics(true) + wrapped.NoFatals(true) + wrapped.ForceLevel(zerolog.PanicLevel) + multiLog(wrapped, "yeet!!") + wrapped.Panic("yeet!!") + }, + shouldPanic: false, + t: t, + }, + } + + for _, test := range panicAndFatalBypassTests { + test.name = "bypass_" + test.name + test.Run() + } + } func ExampleWrap() {