From 9c6eb6a10c3c3386c2fa27d2009a6612328e217c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=9C=C9=B4=E1=B4=8B=C9=B4=E1=B4=A1=E1=B4=8F=C9=B4?= Date: Sun, 22 Nov 2020 23:12:34 +0800 Subject: [PATCH 1/2] Support writing to a named logger --- .github/workflows/go.yml | 2 +- README.md | 16 ++++++ clog.go | 50 +++++++++++++++-- clog_test.go | 115 +++++++++++++++++++++++++++++++-------- logger.go | 42 ++++++++++---- 5 files changed, 187 insertions(+), 38 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 79173ae..61ce240 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: name: Test strategy: matrix: - go-version: [1.13.x, 1.14.x] + go-version: [1.14.x, 1.15.x] platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/README.md b/README.md index ab0553d..f42bf12 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,22 @@ func init() { In this example, all logs will be printed to console, and only logs with level Info or higher (i.e. Warn, Error and Fatal) will be written into file. +### Write to a specific logger + +When multiple loggers are registered, it is also possible to write logs to a special logger by giving its name. + +```go +func main() { + log.TraceTo(log.DefaultConsoleName, "Hello %s!", "World") + log.InfoTo(log.DefaultConsoleName, "Hello %s!", "World") + log.WarnTo(log.DefaultConsoleName, "Hello %s!", "World") + log.ErrorTo(log.DefaultConsoleName, "So bad... %v", err) + log.FatalTo(log.DefaultConsoleName, "Boom! %v", err) + + // ... +} +``` + ### Caller Location When using `log.Error` and `log.Fatal` functions, the caller location is written along with logs. diff --git a/clog.go b/clog.go index a7bcd1e..b8ed751 100644 --- a/clog.go +++ b/clog.go @@ -70,10 +70,7 @@ func Fatal(format string, v ...interface{}) { // In test environment, Fatal or FatalDepth won't stop the manager or exit the program. var isTestEnv = false -// FatalDepth writes formatted log with given skip depth in Fatal level then exits. -func FatalDepth(skip int, format string, v ...interface{}) { - mgr.write(LevelFatal, skip, format, v...) - +func exit() { if isTestEnv { return } @@ -82,6 +79,51 @@ func FatalDepth(skip int, format string, v ...interface{}) { os.Exit(1) } +// FatalDepth writes formatted log with given skip depth in Fatal level then exits. +func FatalDepth(skip int, format string, v ...interface{}) { + mgr.write(LevelFatal, skip, format, v...) + exit() +} + +// TraceTo writes formatted log in Trace level to the logger with given name. +func TraceTo(name, format string, v ...interface{}) { + mgr.writeTo(name, LevelTrace, 0, format, v...) +} + +// InfoTo writes formatted log in Info level to the logger with given name. +func InfoTo(name, format string, v ...interface{}) { + mgr.writeTo(name, LevelInfo, 0, format, v...) +} + +// WarnTo writes formatted log in Warn level to the logger with given name. +func WarnTo(name, format string, v ...interface{}) { + mgr.writeTo(name, LevelWarn, 0, format, v...) +} + +// ErrorTo writes formatted log in Error level to the logger with given name. +func ErrorTo(name, format string, v ...interface{}) { + ErrorDepthTo(name, 4, format, v...) +} + +// ErrorDepthTo writes formatted log with given skip depth in Error level to +// the logger with given name. +func ErrorDepthTo(name string, skip int, format string, v ...interface{}) { + mgr.writeTo(name, LevelError, skip, format, v...) +} + +// FatalTo writes formatted log in Fatal level to the logger with given name +// then exits. +func FatalTo(name, format string, v ...interface{}) { + FatalDepthTo(name, 4, format, v...) +} + +// FatalDepthTo writes formatted log with given skip depth in Fatal level to +// the logger with given name then exits. +func FatalDepthTo(name string, skip int, format string, v ...interface{}) { + mgr.writeTo(name, LevelFatal, skip, format, v...) + exit() +} + // Stop propagates cancellation to all loggers and waits for completion. // This function should always be called before exiting the program. func Stop() { diff --git a/clog_test.go b/clog_test.go index e363fbf..7db9456 100644 --- a/clog_test.go +++ b/clog_test.go @@ -36,36 +36,36 @@ func (l *chanLogger) Write(m Messager) error { return nil } -func Test_chanLogger(t *testing.T) { - initer := func(name string, level Level) Initer { - return func(_ string, vs ...interface{}) (Logger, error) { - var cfg *chanConfig - for i := range vs { - switch v := vs[i].(type) { - case chanConfig: - cfg = &v - } - } - - if cfg == nil { - return nil, fmt.Errorf("config object with the type '%T' not found", chanConfig{}) +func chanLoggerIniter(name string, level Level) Initer { + return func(_ string, vs ...interface{}) (Logger, error) { + var cfg *chanConfig + for i := range vs { + switch v := vs[i].(type) { + case chanConfig: + cfg = &v } + } - return &chanLogger{ - c: cfg.c, - noopLogger: &noopLogger{ - name: name, - level: level, - }, - }, nil + if cfg == nil { + return nil, fmt.Errorf("config object with the type '%T' not found", chanConfig{}) } + + return &chanLogger{ + c: cfg.c, + noopLogger: &noopLogger{ + name: name, + level: level, + }, + }, nil } +} +func Test_chanLogger(t *testing.T) { test1 := "mode1" - test1Initer := initer(test1, LevelTrace) + test1Initer := chanLoggerIniter(test1, LevelTrace) test2 := "mode2" - test2Initer := initer(test2, LevelError) + test2Initer := chanLoggerIniter(test2, LevelError) c1 := make(chan string) c2 := make(chan string) @@ -130,3 +130,74 @@ func Test_chanLogger(t *testing.T) { }) } } + +func Test_writeToNamedLogger(t *testing.T) { + test1 := "alice" + test1Initer := chanLoggerIniter(test1, LevelTrace) + + test2 := "bob" + test2Initer := chanLoggerIniter(test2, LevelTrace) + + c1 := make(chan string) + c2 := make(chan string) + + defer Remove(test1) + defer Remove(test2) + assert.Nil(t, New(test1, test1Initer, 1, chanConfig{ + c: c1, + })) + assert.Nil(t, New(test2, test2Initer, 1, chanConfig{ + c: c2, + })) + + tests := []struct { + name string + fn func(string, string, ...interface{}) + containsStr1 string + containsStr2 string + }{ + { + name: "trace", + fn: TraceTo, + containsStr1: "[TRACE] log message", + containsStr2: "", + }, + { + name: "info", + fn: InfoTo, + containsStr1: "[ INFO] log message", + containsStr2: "", + }, + { + name: "warn", + fn: WarnTo, + containsStr1: "[ WARN] log message", + containsStr2: "", + }, + { + name: "error", + fn: ErrorTo, + containsStr1: "()] log message", + containsStr2: "", + }, + { + name: "fatal", + fn: FatalTo, + containsStr1: "()] log message", + containsStr2: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, 2, mgr.len()) + + tt.fn(test1, "log message") + + assert.Contains(t, <-c1, tt.containsStr1) + + if tt.containsStr2 != "" { + assert.Contains(t, <-c2, tt.containsStr2) + } + }) + } +} diff --git a/logger.go b/logger.go index e1439c8..2660540 100644 --- a/logger.go +++ b/logger.go @@ -55,21 +55,23 @@ func (l *cancelableLogger) error(err error) { } const ( - stateStopping int64 = 0 - stateRunning int64 = 1 + stateStopping int64 = iota + stateRunning ) type manager struct { - state int64 - ctx context.Context - cancel context.CancelFunc - loggers []*cancelableLogger + state int64 + ctx context.Context + cancel context.CancelFunc + loggers []*cancelableLogger + loggersByName map[string]*cancelableLogger } func (m *manager) len() int { return len(m.loggers) } +// write attempts to send message to all loggers. func (m *manager) write(level Level, skip int, format string, v ...interface{}) { if mgr.len() == 0 { errLogger.Print(errSprintf("[clog] no logger is available")) @@ -90,6 +92,21 @@ func (m *manager) write(level Level, skip int, format string, v ...interface{}) } } +// writeTo attempts to send message to the logger with given name. +func (m *manager) writeTo(name string, level Level, skip int, format string, v ...interface{}) { + l, ok := mgr.loggersByName[name] + if !ok { + errLogger.Print(errSprintf("[clog] logger with name %q is not available", name)) + return + } + + if l.Level() > level { + return + } + + l.msgChan <- newMessage(level, skip, format, v...) +} + func (m *manager) stop() { // Make sure cancellation is only propagated once to prevent deadlock of WaitForStop. if !atomic.CompareAndSwapInt64(&m.state, stateRunning, stateStopping) { @@ -107,9 +124,10 @@ var mgr *manager func init() { ctx, cancel := context.WithCancel(context.Background()) mgr = &manager{ - state: stateRunning, - ctx: ctx, - cancel: cancel, + state: stateRunning, + ctx: ctx, + cancel: cancel, + loggersByName: make(map[string]*cancelableLogger), } } @@ -124,7 +142,7 @@ type Initer func(string, ...interface{}) (Logger, error) // Any integer type (i.e. int, int32, int64) will be used as buffer size. // Otherwise, the value will be passed to the initer. // -// This function is not concurrent safe. +// NOTE: This function is not concurrent safe. func New(name string, initer Initer, opts ...interface{}) error { bufferSize := 0 @@ -176,6 +194,7 @@ func New(name string, initer Initer, opts ...interface{}) error { if !found { mgr.loggers = append(mgr.loggers, cl) } + mgr.loggersByName[name] = cl go func() { loop: @@ -205,7 +224,7 @@ func New(name string, initer Initer, opts ...interface{}) error { // Remove removes a logger with given name from the managed list. // -// This function is not concurrent safe. +// NOTE: This function is not concurrent safe. func Remove(name string) { loggers := mgr.loggers[:0] for _, l := range mgr.loggers { @@ -219,4 +238,5 @@ func Remove(name string) { loggers = append(loggers, l) } mgr.loggers = loggers + delete(mgr.loggersByName, name) } From 1e1694bf3f0bb84aeeee3cdbd7ac3bcd2752930c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=9C=C9=B4=E1=B4=8B=C9=B4=E1=B4=A1=E1=B4=8F=C9=B4?= Date: Sun, 22 Nov 2020 23:19:11 +0800 Subject: [PATCH 2/2] Format README --- README.md | 89 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f42bf12..3f2f6a5 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,26 @@ The code inside `init` function is equivalent to the following: ```go func init() { - err := log.NewConsole(0, log.ConsoleConfig{ - Level: log.LevelTrace, - }) + err := log.NewConsole(0, + log.ConsoleConfig{ + Level: log.LevelTrace, + }, + ) + if err != nil { + panic("unable to create new logger: " + err.Error()) + } +} +``` + +Or expand further: + +```go +func init() { + err := log.NewConsoleWithName(log.DefaultConsoleName, 0, + log.ConsoleConfig{ + Level: log.LevelTrace, + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) } @@ -70,9 +87,11 @@ In production, you may want to make log less verbose and be asynchronous: func init() { // The buffer size mainly depends on number of logs could be produced at the same time, // 100 is a good default. - err := log.NewConsole(100, log.ConsoleConfig{ - Level: log.LevelInfo, - }) + err := log.NewConsole(100, + log.ConsoleConfig{ + Level: log.LevelInfo, + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) } @@ -94,10 +113,12 @@ func init() { if err != nil { panic("unable to create new logger: " + err.Error()) } - err := log.NewFile(log.FileConfig{ - Level: log.LevelInfo, - Filename: "clog.log", - }) + err := log.NewFile( + log.FileConfig{ + Level: log.LevelInfo, + Filename: "clog.log", + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) } @@ -150,14 +171,16 @@ File logger is the single most powerful builtin logger, it has the ability to ro ```go func init() { - err := log.NewFile(100, log.FileConfig{ - Level: log.LevelInfo, - Filename: "clog.log", - FileRotationConfig: log.FileRotationConfig { - Rotate: true, - Daily: true, - }, - }) + err := log.NewFile(100, + log.FileConfig{ + Level: log.LevelInfo, + Filename: "clog.log", + FileRotationConfig: log.FileRotationConfig { + Rotate: true, + Daily: true, + }, + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) } @@ -168,10 +191,12 @@ In case you have some other packages that write to a file, and you want to take ```go func init() { - w, err := log.NewFileWriter("filename", log.FileRotationConfig{ - Rotate: true, - Daily: true, - }) + w, err := log.NewFileWriter("filename", + log.FileRotationConfig{ + Rotate: true, + Daily: true, + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) } @@ -184,10 +209,12 @@ Slack logger is also supported in a simple way: ```go func init() { - err := log.NewSlack(100, log.SlackConfig{ - Level: log.LevelInfo, - URL: "https://url-to-slack-webhook", - }) + err := log.NewSlack(100, + log.SlackConfig{ + Level: log.LevelInfo, + URL: "https://url-to-slack-webhook", + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) } @@ -202,10 +229,12 @@ Discord logger is supported in rich format via [Embed Object](https://discordapp ```go func init() { - err := log.NewDiscord(100, log.DiscordConfig{ - Level: log.LevelInfo, - URL: "https://url-to-discord-webhook", - }) + err := log.NewDiscord(100, + log.DiscordConfig{ + Level: log.LevelInfo, + URL: "https://url-to-discord-webhook", + }, + ) if err != nil { panic("unable to create new logger: " + err.Error()) }