diff --git a/common.go b/common.go index d52a89f..c7f0c01 100644 --- a/common.go +++ b/common.go @@ -270,7 +270,7 @@ var exitHandlers = make([]func(), 0) func runExitHandlers() { defer func() { if err := recover(); err != nil { - printlnStderr("slog: run exit handler error:", err) + printlnStderr("slog: run exit handler(global) recovered, error:", err) } }() diff --git a/common_test.go b/common_test.go index 4a757db..be24db8 100644 --- a/common_test.go +++ b/common_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/gookit/goutil/byteutil" "github.com/gookit/goutil/dump" "github.com/gookit/goutil/errorx" "github.com/gookit/goutil/testutil/assert" @@ -122,6 +123,8 @@ func (w *closedBuffer) StringReset() string { } type testHandler struct { + slog.FormatterWrapper + byteutil.Buffer errOnHandle bool errOnFlush bool errOnClose bool @@ -131,28 +134,38 @@ func newTestHandler() *testHandler { return &testHandler{} } -func (h testHandler) IsHandling(_ slog.Level) bool { +func (h *testHandler) IsHandling(_ slog.Level) bool { return true } -func (h testHandler) Close() error { +func (h *testHandler) Close() error { if h.errOnClose { return errorx.Raw("close error") } + + h.Reset() return nil } -func (h testHandler) Flush() error { +func (h *testHandler) Flush() error { if h.errOnFlush { return errorx.Raw("flush error") } + + h.Reset() return nil } -func (h testHandler) Handle(_ *slog.Record) error { +func (h *testHandler) Handle(r *slog.Record) error { if h.errOnHandle { return errorx.Raw("handle error") } + + bs, err := h.Format(r) + if err != nil { + return err + } + h.Write(bs) return nil } diff --git a/formatter_test.go b/formatter_test.go index 56c1249..b8dab92 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -95,7 +95,7 @@ func TestTextFormatter_Format(t *testing.T) { assert.NotContains(t, logTxt, "}}") } -func TestJSONFormatter(t *testing.T) { +func TestNewJSONFormatter(t *testing.T) { f := slog.NewJSONFormatter() f.AddField(slog.FieldKeyTimestamp) diff --git a/logger.go b/logger.go index 26bc307..b61600b 100644 --- a/logger.go +++ b/logger.go @@ -4,6 +4,8 @@ import ( "context" "sync" "time" + + "github.com/gookit/goutil" ) // Logger log dispatcher definition. @@ -15,6 +17,8 @@ type Logger struct { mu sync.Mutex // logger latest error err error + // mark logger is closed + closed bool // log handlers for logger handlers []Handler @@ -220,9 +224,7 @@ func (l *Logger) Flush() error { return l.lockAndFlushAll() } // MustFlush flush logs. will panic on error func (l *Logger) MustFlush() { - if err := l.lockAndFlushAll(); err != nil { - panic(err) - } + goutil.PanicErr(l.lockAndFlushAll()) } // FlushAll flushes all the logs and attempts to "sync" their data to disk. @@ -251,10 +253,22 @@ func (l *Logger) flushAll() { }) } -// Close the logger +// MustClose close logger. will panic on error +func (l *Logger) MustClose() { + goutil.PanicErr(l.Close()) +} + +// Close the logger, will flush all logs and close all handlers +// +// IMPORTANT: +// +// if enable async/buffer mode, please call the Close() before exit. func (l *Logger) Close() error { + if l.closed { + return nil + } + _ = l.VisitAll(func(handler Handler) error { - // flush logs and then close if err := handler.Close(); err != nil { l.err = err printlnStderr("slog: call handler.Close() error:", err) @@ -262,13 +276,14 @@ func (l *Logger) Close() error { return nil }) + l.closed = true return l.err } // VisitAll logger handlers func (l *Logger) VisitAll(fn func(handler Handler) error) error { for _, handler := range l.handlers { - // you can return nil for ignore error + // TIP: you can return nil for ignore error if err := fn(handler); err != nil { return err } @@ -276,8 +291,9 @@ func (l *Logger) VisitAll(fn func(handler Handler) error) error { return nil } -// Reset the logger +// Reset the logger. will reset: handlers, processors, closed=false func (l *Logger) Reset() { + l.closed = false l.ResetHandlers() l.ResetProcessors() } @@ -307,7 +323,7 @@ func (l *Logger) Exit(code int) { func (l *Logger) runExitHandlers() { defer func() { if err := recover(); err != nil { - printlnStderr("slog: run exit handler error:", err) + printlnStderr("slog: run exit handler recovered, error:", err) } }() @@ -316,14 +332,18 @@ func (l *Logger) runExitHandlers() { } } -// DoNothingOnPanicFatal do nothing on panic or fatal level. -// useful on testing. +// DoNothingOnPanicFatal do nothing on panic or fatal level. useful on testing. func (l *Logger) DoNothingOnPanicFatal() { l.PanicFunc = DoNothingOnPanic l.ExitFunc = DoNothingOnExit } -// LastErr fetch, will clear after read. +// HandlersNum returns the number of handlers +func (l *Logger) HandlersNum() int { + return len(l.handlers) +} + +// LastErr fetch, will clear it after read. func (l *Logger) LastErr() error { err := l.err l.err = nil diff --git a/logger_test.go b/logger_test.go index 5b80dbb..4ca13ee 100644 --- a/logger_test.go +++ b/logger_test.go @@ -47,6 +47,7 @@ func TestLogger_PushHandler(t *testing.T) { l.MustFlush() assert.NoErr(t, l.Close()) + l.MustClose() l.Reset() } @@ -111,6 +112,15 @@ func TestLogger_panic(t *testing.T) { err := l.LastErr() assert.Err(t, err) assert.Eq(t, "flush error", err.Error()) + + h.errOnClose = true + assert.Panics(t, func() { + l.MustClose() + }) + + err = l.LastErr() + assert.Err(t, err) + assert.Eq(t, "close error", err.Error()) } func TestLogger_error(t *testing.T) { diff --git a/rotatefile/rotatefile_test.go b/rotatefile/rotatefile_test.go new file mode 100644 index 0000000..db407ce --- /dev/null +++ b/rotatefile/rotatefile_test.go @@ -0,0 +1,28 @@ +package rotatefile_test + +import ( + "fmt" + "log" + "testing" + + "github.com/gookit/goutil" + "github.com/gookit/goutil/fsutil" + "github.com/gookit/slog/rotatefile" +) + +func TestMain(m *testing.M) { + fmt.Println("TestMain: remove all test files in ./testdata") + goutil.PanicErr(fsutil.RemoveSub("./testdata", fsutil.ExcludeNames(".keep"))) + m.Run() +} + +func ExampleNewWriter_on_other_logger() { + logFile := "testdata/another_logger.log" + writer, err := rotatefile.NewConfig(logFile).Create() + if err != nil { + panic(err) + } + + log.SetOutput(writer) + log.Println("log message") +} diff --git a/slog.go b/slog.go index cd1886a..ec88e00 100644 --- a/slog.go +++ b/slog.go @@ -55,7 +55,7 @@ var std = NewStdLogger() // Std get std logger func Std() *SugaredLogger { return std } -// Reset the std logger +// Reset the std logger and reset exit handlers func Reset() { ResetExitHandlers(true) // new std @@ -68,9 +68,19 @@ func Configure(fn func(l *SugaredLogger)) { std.Config(fn) } // SetExitFunc to the std logger func SetExitFunc(fn func(code int)) { std.ExitFunc = fn } -// Exit runs all the logger exit handlers and then terminates the program using os.Exit(code) +// Exit runs all exit handlers and then terminates the program using os.Exit(code) func Exit(code int) { std.Exit(code) } +// Close logger, flush and close all handlers. +// +// IMPORTANT: please call Close() before app exit. +func Close() error { return std.Close() } + +// MustClose logger, flush and close all handlers. +// +// IMPORTANT: please call Close() before app exit. +func MustClose() { goutil.PanicErr(Close()) } + // Flush log messages func Flush() error { return std.Flush() } @@ -82,7 +92,7 @@ func FlushTimeout(timeout time.Duration) { std.FlushTimeout(timeout) } // FlushDaemon run flush handle on daemon. // -// Usage please see ExampleFlushDaemon() +// Usage please see slog_test.ExampleFlushDaemon() func FlushDaemon(onStops ...func()) { std.FlushDaemon(onStops...) } diff --git a/slog_test.go b/slog_test.go index 49b1f27..1bee1e3 100644 --- a/slog_test.go +++ b/slog_test.go @@ -2,6 +2,7 @@ package slog_test import ( "bytes" + "context" "errors" "fmt" "strconv" @@ -101,6 +102,7 @@ func TestFlushTimeout(t *testing.T) { defer slog.Reset() slog.Info("print log message") slog.FlushTimeout(timex.Second * 1) + slog.MustFlush() } func TestNewSugaredLogger(t *testing.T) { @@ -191,16 +193,32 @@ func printfLogs(msg string, args ...any) { slog.Fatalf(msg, args...) } -func TestUseJSONFormat(t *testing.T) { +func TestSetFormatter_jsonFormat(t *testing.T) { defer slog.Reset() + slog.SetLogLevel(slog.TraceLevel) slog.SetFormatter(slog.NewJSONFormatter()) - slog.Info("info log message") - slog.Warn("warning log message") + th := newTestHandler() + th.SetFormatter(slog.NewJSONFormatter().Configure(func(f *slog.JSONFormatter) { + f.Fields = slog.NoTimeFields + })) + slog.PushHandler(th) + + assert.Eq(t, 2, slog.Std().HandlersNum()) + + slog.Info("info log message1") + slog.Warn("warning log message2") + s := th.ResetGet() + assert.StrContains(t, s, `"level":"INFO"`) + assert.StrContains(t, s, `info log message1`) + assert.StrContains(t, s, `"level":"WARN"`) + assert.StrContains(t, s, `warning log message2`) + slog.WithData(slog.M{ "key0": 134, "key1": "abc", }).Infof("info log %s", "message") + s = th.ResetGet() r := slog.WithFields(slog.M{ "category": "service", @@ -208,6 +226,17 @@ func TestUseJSONFormat(t *testing.T) { }) r.Infof("info %s", "message") r.Debugf("debug %s", "message") + s = th.ResetGet() + + r = slog.WithField("app", "order") + r.Trace("trace message") + r.Println("print message") + s = th.ResetGet() + assert.StrContains(t, s, `"app":"order"`) + assert.StrCount(t, s, `"app":"order"`, 2) + + slog.WithContext(context.Background()).Print("print message with ctx") + assert.StrContains(t, th.ResetGet(), "print message with ctx") } func TestAddHandler(t *testing.T) { @@ -344,7 +373,7 @@ func TestExitHandlerWithError(t *testing.T) { testutil.RewriteStderr() slog.Exit(23) str := testutil.RestoreStderr() - assert.Eq(t, "slog: run exit handler error: test error\n", str) + assert.Eq(t, "slog: run exit handler(global) recovered, error: test error\n", str) } func TestLogger_ExitHandlerWithError(t *testing.T) { @@ -361,7 +390,7 @@ func TestLogger_ExitHandlerWithError(t *testing.T) { testutil.RewriteStderr() l.Exit(23) str := testutil.RestoreStderr() - assert.Eq(t, "slog: run exit handler error: test error\n", str) + assert.Eq(t, "slog: run exit handler recovered, error: test error\n", str) } func TestLogger_PrependExitHandler(t *testing.T) { @@ -378,7 +407,7 @@ func TestLogger_PrependExitHandler(t *testing.T) { testutil.RewriteStderr() l.Exit(23) str := testutil.RestoreStderr() - assert.Eq(t, "slog: run exit handler error: test error2\n", str) + assert.Eq(t, "slog: run exit handler recovered, error: test error2\n", str) } func TestSugaredLogger_Close(t *testing.T) {