diff --git a/pkg/log/common_test.go b/pkg/log/common_test.go index 3cf7d58..c244103 100644 --- a/pkg/log/common_test.go +++ b/pkg/log/common_test.go @@ -37,7 +37,7 @@ func assertFollowAllInterfaces(t *testing.T, logger Logger) { assertSilentLoggerProviderFollowFormatLnInterface(t, logger) }) - t.Run("Format Ln logger", func(t *testing.T) { + t.Run("Format with Ln logger", func(t *testing.T) { assertFollowFormatLnInterface(t, logger) }) @@ -49,18 +49,18 @@ func assertFollowAllInterfaces(t *testing.T, logger Logger) { func assertFollowFormatLnInterface(t *testing.T, logger Logger) { runs := []func(){ func() { - logger.InfoFLn("INFO %s", "test_info") + logger.InfoF("INFO %s", "test_info") }, func() { - logger.WarnFLn("WARN %s", "test_warn") + logger.WarnF("WARN %s", "test_warn") }, func() { - logger.DebugFLn("DEBUG %s", "test_debug") + logger.DebugF("DEBUG %s", "test_debug") }, func() { - logger.ErrorFLn("ERROR %v", fmt.Errorf("test_error")) + logger.ErrorF("ERROR %v", fmt.Errorf("test_error")) }, } diff --git a/pkg/log/dummy.go b/pkg/log/dummy.go index 93c307c..c8bfc94 100644 --- a/pkg/log/dummy.go +++ b/pkg/log/dummy.go @@ -66,7 +66,7 @@ func (d *DummyLogger) Process(_ Process, t string, run func() error) error { return err } -func (d *DummyLogger) InfoF(format string, a ...interface{}) { +func (d *DummyLogger) InfoFWithoutLn(format string, a ...interface{}) { fmt.Printf(format, a...) } @@ -74,7 +74,7 @@ func (d *DummyLogger) InfoLn(a ...interface{}) { fmt.Println(a...) } -func (d *DummyLogger) ErrorF(format string, a ...interface{}) { +func (d *DummyLogger) ErrorFWithoutLn(format string, a ...interface{}) { fmt.Printf(format, a...) } @@ -82,7 +82,7 @@ func (d *DummyLogger) ErrorLn(a ...interface{}) { fmt.Println(a...) } -func (d *DummyLogger) DebugF(format string, a ...interface{}) { +func (d *DummyLogger) DebugFWithoutLn(format string, a ...interface{}) { if d.isDebug { fmt.Printf(format, a...) } @@ -110,7 +110,7 @@ func (d *DummyLogger) WarnLn(a ...interface{}) { fmt.Println(a...) } -func (d *DummyLogger) WarnF(format string, a ...interface{}) { +func (d *DummyLogger) WarnFWithoutLn(format string, a ...interface{}) { fmt.Printf(format, a...) } diff --git a/pkg/log/in_memory.go b/pkg/log/in_memory.go index 559c2b2..a83539f 100644 --- a/pkg/log/in_memory.go +++ b/pkg/log/in_memory.go @@ -167,9 +167,9 @@ func (l *InMemoryLogger) Process(p Process, t string, action func() error) error return err } -func (l *InMemoryLogger) InfoF(format string, a ...interface{}) { +func (l *InMemoryLogger) InfoFWithoutLn(format string, a ...interface{}) { l.writeEntityFormatted(format, a...) - l.parent.InfoF(format, a...) + l.parent.InfoFWithoutLn(format, a...) } func (l *InMemoryLogger) InfoLn(a ...interface{}) { @@ -177,9 +177,9 @@ func (l *InMemoryLogger) InfoLn(a ...interface{}) { l.parent.InfoLn(a...) } -func (l *InMemoryLogger) ErrorF(format string, a ...interface{}) { +func (l *InMemoryLogger) ErrorFWithoutLn(format string, a ...interface{}) { l.writeEntityWithPrefix(l.errorPrefix, format, a...) - l.parent.ErrorF(format, a...) + l.parent.ErrorFWithoutLn(format, a...) } func (l *InMemoryLogger) ErrorLn(a ...interface{}) { @@ -187,13 +187,13 @@ func (l *InMemoryLogger) ErrorLn(a ...interface{}) { l.parent.ErrorLn(a...) } -func (l *InMemoryLogger) DebugF(format string, a ...interface{}) { +func (l *InMemoryLogger) DebugFWithoutLn(format string, a ...interface{}) { if l.notDebug { return } l.writeEntityWithPrefix(l.debugPrefix, format, a...) - l.parent.DebugF(format, a...) + l.parent.DebugFWithoutLn(format, a...) } func (l *InMemoryLogger) DebugLn(a ...interface{}) { @@ -205,9 +205,9 @@ func (l *InMemoryLogger) DebugLn(a ...interface{}) { l.parent.DebugLn(a...) } -func (l *InMemoryLogger) WarnF(format string, a ...interface{}) { +func (l *InMemoryLogger) WarnFWithoutLn(format string, a ...interface{}) { l.writeEntityFormatted(format, a...) - l.parent.WarnF(format, a...) + l.parent.WarnFWithoutLn(format, a...) } func (l *InMemoryLogger) WarnLn(a ...interface{}) { diff --git a/pkg/log/ln_logger_wrapper.go b/pkg/log/ln_logger_wrapper.go index 25ebbb5..2d1b0aa 100644 --- a/pkg/log/ln_logger_wrapper.go +++ b/pkg/log/ln_logger_wrapper.go @@ -27,20 +27,20 @@ func newFormatWithNewLineLoggerWrapper(parent baseLogger) *formatWithNewLineLogg return &formatWithNewLineLoggerWrapper{parent: parent} } -func (w *formatWithNewLineLoggerWrapper) InfoFLn(format string, a ...any) { - w.parent.InfoF(addLnToMessage(format, a...)) +func (w *formatWithNewLineLoggerWrapper) InfoF(format string, a ...any) { + w.parent.InfoFWithoutLn(addLnToMessage(format, a...)) } -func (w *formatWithNewLineLoggerWrapper) ErrorFLn(format string, a ...any) { - w.parent.ErrorF(addLnToMessage(format, a...)) +func (w *formatWithNewLineLoggerWrapper) ErrorF(format string, a ...any) { + w.parent.ErrorFWithoutLn(addLnToMessage(format, a...)) } -func (w *formatWithNewLineLoggerWrapper) DebugFLn(format string, a ...any) { - w.parent.DebugF(addLnToMessage(format, a...)) +func (w *formatWithNewLineLoggerWrapper) DebugF(format string, a ...any) { + w.parent.DebugFWithoutLn(addLnToMessage(format, a...)) } -func (w *formatWithNewLineLoggerWrapper) WarnFLn(format string, a ...any) { - w.parent.WarnF(addLnToMessage(format, a...)) +func (w *formatWithNewLineLoggerWrapper) WarnF(format string, a ...any) { + w.parent.WarnFWithoutLn(addLnToMessage(format, a...)) } func addLnToMessage(format string, a ...any) string { diff --git a/pkg/log/ln_logger_wrapper_test.go b/pkg/log/ln_logger_wrapper_test.go new file mode 100644 index 0000000..3b9cf18 --- /dev/null +++ b/pkg/log/ln_logger_wrapper_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 Flant JSC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLnLoggerWrapper(t *testing.T) { + logger := NewInMemoryLoggerWithParent(NewSimpleLogger(LoggerOptions{IsDebug: true})) + + assertAddNewLine := func(t *testing.T, msg string) { + matches, err := logger.AllMatches(&Match{ + Prefix: []string{fmt.Sprintf("%s\n", msg)}, + }) + + require.NoError(t, err) + require.Len(t, matches, 1, msg) + } + + wrapper := newFormatWithNewLineLoggerWrapper(logger) + + wrapper.ErrorF("Error") + assertAddNewLine(t, "Error") + + wrapper.WarnF("Warn") + assertAddNewLine(t, "Warn") + + wrapper.InfoF("Info") + assertAddNewLine(t, "Info") + + wrapper.DebugF("Debug") + assertAddNewLine(t, "Debug") +} diff --git a/pkg/log/logger.go b/pkg/log/logger.go index 0508052..72a7abc 100644 --- a/pkg/log/logger.go +++ b/pkg/log/logger.go @@ -107,16 +107,16 @@ type baseLogger interface { Process(Process, string, func() error) error - InfoF(format string, a ...interface{}) + InfoFWithoutLn(format string, a ...interface{}) InfoLn(a ...interface{}) - ErrorF(format string, a ...interface{}) + ErrorFWithoutLn(format string, a ...interface{}) ErrorLn(a ...interface{}) - DebugF(format string, a ...interface{}) + DebugFWithoutLn(format string, a ...interface{}) DebugLn(a ...interface{}) - WarnF(format string, a ...interface{}) + WarnFWithoutLn(format string, a ...interface{}) WarnLn(a ...interface{}) Success(string) @@ -129,11 +129,27 @@ type baseLogger interface { ProcessLogger() ProcessLogger } +// formatWithNewLineLogger +// Because we are using pretty printing in dhctl +// we huge usage InfoF("msg\n") with \n in end of line +// and often forget to add \n in message type formatWithNewLineLogger interface { - InfoFLn(format string, a ...any) - ErrorFLn(format string, a ...any) - DebugFLn(format string, a ...any) - WarnFLn(format string, a ...any) + // InfoF + // Warning! InfoF add \n to end of message. + // If you do not have \n to end of message please use InfoFWithoutLn + InfoF(format string, a ...any) + // ErrorF + // Warning! ErrorF add \n to end of message. + // If you do not have \n to end of message please use ErrorFWithoutLn + ErrorF(format string, a ...any) + // DebugF + // Warning! DebugF add \n to end of message. + // If you do not have \n to end of message please use DebugFWithoutLn + DebugF(format string, a ...any) + // WarnF + // Warning! WarnF add \n to end of message. + // If you do not have \n to end of message please use WarnFWithoutLn + WarnF(format string, a ...any) } type Logger interface { diff --git a/pkg/log/pretty.go b/pkg/log/pretty.go index 40486ff..4ae8708 100644 --- a/pkg/log/pretty.go +++ b/pkg/log/pretty.go @@ -117,7 +117,7 @@ func (d *PrettyLogger) Process(p Process, t string, run func() error) error { return d.logboekLogger.LogProcess(format.Title, t).Options(format.OptionsSetter).DoError(run) } -func (d *PrettyLogger) InfoF(format string, a ...interface{}) { +func (d *PrettyLogger) InfoFWithoutLn(format string, a ...interface{}) { d.logboekLogger.Info().LogF(format, a...) } @@ -125,7 +125,7 @@ func (d *PrettyLogger) InfoLn(a ...interface{}) { d.logboekLogger.Info().LogLn(a...) } -func (d *PrettyLogger) ErrorF(format string, a ...interface{}) { +func (d *PrettyLogger) ErrorFWithoutLn(format string, a ...interface{}) { d.logboekLogger.Error().LogF(format, a...) } @@ -133,7 +133,7 @@ func (d *PrettyLogger) ErrorLn(a ...interface{}) { d.logboekLogger.Error().LogLn(a...) } -func (d *PrettyLogger) DebugF(format string, a ...interface{}) { +func (d *PrettyLogger) DebugFWithoutLn(format string, a ...interface{}) { if d.debugLogWriter != nil { o := fmt.Sprintf(format, a...) _, err := d.debugLogWriter.DebugStream.Write([]byte(o)) @@ -162,11 +162,11 @@ func (d *PrettyLogger) DebugLn(a ...interface{}) { } func (d *PrettyLogger) Success(l string) { - d.InfoF("🎉 %s", l) + d.InfoFWithoutLn("🎉 %s", l) } func (d *PrettyLogger) Fail(l string) { - d.InfoF("️⛱️️ %s", l) + d.InfoFWithoutLn("️⛱️️ %s", l) } func (d *PrettyLogger) FailRetry(l string) { @@ -178,9 +178,9 @@ func (d *PrettyLogger) WarnLn(a ...interface{}) { d.InfoLn(color.New(color.Bold).Sprint(a...)) } -func (d *PrettyLogger) WarnF(format string, a ...interface{}) { +func (d *PrettyLogger) WarnFWithoutLn(format string, a ...interface{}) { line := color.New(color.Bold).Sprintf("❗ ~ "+format, a...) - d.InfoF(line) + d.InfoFWithoutLn(line) } func (d *PrettyLogger) JSON(content []byte) { @@ -188,7 +188,7 @@ func (d *PrettyLogger) JSON(content []byte) { } func (d *PrettyLogger) Write(content []byte) (int, error) { - d.InfoF(string(content)) + d.InfoFWithoutLn(string(content)) return len(content), nil } diff --git a/pkg/log/pretty_test.go b/pkg/log/pretty_test.go index 1cc277b..dafcf7d 100644 --- a/pkg/log/pretty_test.go +++ b/pkg/log/pretty_test.go @@ -182,7 +182,7 @@ func testPrettyLoggerProcess(t *testing.T, tst *testPrettyLogger) { inRunMsg := fmt.Sprintf("run in process: %s", string(tst.process)) err := tst.logger.Process(tst.process, processName, func() error { - tst.logger.InfoFLn(inRunMsg) + tst.logger.InfoF(inRunMsg) return nil }) diff --git a/pkg/log/silent.go b/pkg/log/silent.go index 6dca0c9..fa31259 100644 --- a/pkg/log/silent.go +++ b/pkg/log/silent.go @@ -68,7 +68,7 @@ func (d *SilentLogger) FlushAndClose() error { return nil } -func (d *SilentLogger) InfoF(format string, a ...interface{}) { +func (d *SilentLogger) InfoFWithoutLn(format string, a ...interface{}) { if d.t != nil { d.t.writeToFile(fmt.Sprintf(format, a...)) } @@ -80,7 +80,7 @@ func (d *SilentLogger) InfoLn(a ...interface{}) { } } -func (d *SilentLogger) ErrorF(format string, a ...interface{}) { +func (d *SilentLogger) ErrorFWithoutLn(format string, a ...interface{}) { if d.t != nil { d.t.writeToFile(fmt.Sprintf(format, a...)) } @@ -92,7 +92,7 @@ func (d *SilentLogger) ErrorLn(a ...interface{}) { } } -func (d *SilentLogger) DebugF(format string, a ...interface{}) { +func (d *SilentLogger) DebugFWithoutLn(format string, a ...interface{}) { if d.t != nil { d.t.writeToFile(fmt.Sprintf(format, a...)) } @@ -128,7 +128,7 @@ func (d *SilentLogger) WarnLn(a ...interface{}) { } } -func (d *SilentLogger) WarnF(format string, a ...interface{}) { +func (d *SilentLogger) WarnFWithoutLn(format string, a ...interface{}) { if d.t != nil { d.t.writeToFile(fmt.Sprintf(format, a...)) } diff --git a/pkg/log/simple.go b/pkg/log/simple.go index a9ea06e..feddb63 100644 --- a/pkg/log/simple.go +++ b/pkg/log/simple.go @@ -80,7 +80,7 @@ func (d *SimpleLogger) Process(p Process, t string, run func() error) error { return err } -func (d *SimpleLogger) InfoF(format string, a ...interface{}) { +func (d *SimpleLogger) InfoFWithoutLn(format string, a ...interface{}) { d.logger.Info(format, a...) } @@ -88,7 +88,7 @@ func (d *SimpleLogger) InfoLn(a ...interface{}) { d.logger.Info(listToString(a)) } -func (d *SimpleLogger) ErrorF(format string, a ...interface{}) { +func (d *SimpleLogger) ErrorFWithoutLn(format string, a ...interface{}) { d.logger.Error(format, a...) } @@ -96,7 +96,7 @@ func (d *SimpleLogger) ErrorLn(a ...interface{}) { d.logger.Error(listToString(a)) } -func (d *SimpleLogger) DebugF(format string, a ...interface{}) { +func (d *SimpleLogger) DebugFWithoutLn(format string, a ...interface{}) { if d.isDebug { d.logger.Debug(format, a...) } @@ -121,7 +121,7 @@ func (d *SimpleLogger) FailRetry(l string) { d.logger.With("status", "FAIL").Warn(l) } -func (d *SimpleLogger) WarnF(format string, a ...interface{}) { +func (d *SimpleLogger) WarnFWithoutLn(format string, a ...interface{}) { d.logger.Warn(format, a...) } diff --git a/pkg/log/slog.go b/pkg/log/slog.go new file mode 100644 index 0000000..346e7e6 --- /dev/null +++ b/pkg/log/slog.go @@ -0,0 +1,189 @@ +// Copyright 2026 Flant JSC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "context" + "fmt" + "log/slog" + "strings" +) + +type SLogHandler struct { + loggerProvider LoggerProvider + + attrs []slog.Attr + attrsString string + group string + + prefix string + isDebug bool +} + +func NewSLogHandler(provider LoggerProvider) *SLogHandler { + return NewSLogHandlerWithPrefix(provider, "") +} + +func NewSLogHandlerWithPrefix(provider LoggerProvider, prefix string) *SLogHandler { + return &SLogHandler{ + loggerProvider: provider, + prefix: prefix, + } +} + +func NewSLogWithDebug(ctx context.Context, provider LoggerProvider, isDebug bool) *slog.Logger { + return NewSLogWithPrefixAndDebug(ctx, provider, "", isDebug) +} + +func NewSLogWithPrefix(ctx context.Context, provider LoggerProvider, prefix string) *slog.Logger { + return NewSLogWithPrefixAndDebug(ctx, provider, prefix, false) +} + +func NewSLogWithPrefixAndDebug(ctx context.Context, provider LoggerProvider, prefix string, isDebug bool) *slog.Logger { + handler := NewSLogHandlerWithPrefix(provider, prefix).WithDebug(isDebug) + + logger := slog.New(handler) + lvl := slog.LevelInfo + if isDebug { + lvl = slog.LevelDebug + } + logger.Enabled(ctx, lvl) + + return logger +} + +func (h *SLogHandler) WithPrefix(p string) *SLogHandler { + h.prefix = p + + return h +} + +func (h *SLogHandler) WithDebug(d bool) *SLogHandler { + h.isDebug = d + + return h +} + +func copyHandler(h *SLogHandler) *SLogHandler { + return &SLogHandler{ + loggerProvider: h.loggerProvider, + attrsString: h.attrsString, + group: h.group, + prefix: h.prefix, + isDebug: h.isDebug, + } +} + +func attrsToString(attrs []slog.Attr) string { + if len(attrs) == 0 { + return "" + } + + builder := strings.Builder{} + for _, attr := range attrs { + builder.WriteString(attr.Key) + builder.WriteString("=") + builder.WriteString(fmt.Sprintf(`'%s' `, attr.Value.String())) + } + + return fmt.Sprintf(" | attributes: [%s]", strings.TrimRight(builder.String(), " ")) +} + +func copyAttrs(attrs []slog.Attr) []slog.Attr { + if len(attrs) == 0 { + return nil + } + + cpy := make([]slog.Attr, len(attrs)) + copy(cpy, attrs) + return cpy +} + +func newHandlerWithAttrs(parent *SLogHandler, attrs []slog.Attr) *SLogHandler { + a := append(copyAttrs(parent.attrs), attrs...) + + res := copyHandler(parent) + res.attrsString = attrsToString(a) + + return res +} + +func newHandlerWithGroup(parent *SLogHandler, group string) *SLogHandler { + g := group + if parent.group != "" { + g = parent.group + "/" + group + } + + res := copyHandler(parent) + res.group = g + + return res +} + +func (h *SLogHandler) Enabled(_ context.Context, lvl slog.Level) bool { + if h.isDebug { + // handle all + return true + } + + return lvl >= slog.LevelInfo +} + +func (h *SLogHandler) Handle(_ context.Context, record slog.Record) error { + logger := SafeProvideLogger(h.loggerProvider) + write := logger.DebugF + switch record.Level { + case slog.LevelDebug: + write = logger.DebugF + case slog.LevelInfo: + write = logger.InfoF + case slog.LevelWarn: + write = logger.WarnF + case slog.LevelError: + write = logger.ErrorF + } + + write(h.message(record.Message)) + + return nil +} + +func (h *SLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return newHandlerWithAttrs(h, attrs) +} + +func (h *SLogHandler) WithGroup(name string) slog.Handler { + return newHandlerWithGroup(h, name) +} + +func (h *SLogHandler) message(msg string) string { + totalMsg := strings.Builder{} + if h.prefix != "" { + totalMsg.WriteString(fmt.Sprintf("%s: %s", h.prefix, msg)) + } else { + totalMsg.WriteString(msg) + } + + if h.group != "" { + totalMsg.WriteString(fmt.Sprintf(" | groups: '%s'", h.group)) + } + + if h.attrsString != "" { + // h.attrs contains leading space and | before attributes + totalMsg.WriteString(h.attrsString) + } + + return totalMsg.String() +} diff --git a/pkg/log/slog_test.go b/pkg/log/slog_test.go new file mode 100644 index 0000000..143ba1a --- /dev/null +++ b/pkg/log/slog_test.go @@ -0,0 +1,263 @@ +// Copyright 2026 Flant JSC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package log + +import ( + "context" + "fmt" + "log/slog" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDebugLogger(t *testing.T) { + assertSimpleMessage := func(t *testing.T, logger *InMemoryLogger, msg string, hasInLog bool) { + matches, err := logger.AllMatches(&Match{ + Prefix: []string{msg}, + }) + + expectedLen := 0 + if hasInLog { + expectedLen = 1 + } + + require.NoError(t, err) + require.Len(t, matches, expectedLen, "message: '%s'", msg) + } + + type test struct { + name string + msg string + writeLog func(logger *slog.Logger, msg string) + } + + t.Run("with debug", func(t *testing.T) { + testsOneShotLogs := []test{ + { + name: "debug", + msg: "debug message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Debug(msg) + }, + }, + + { + name: "info", + msg: "info message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Info(msg) + }, + }, + { + name: "warning", + msg: "warn message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Warn(msg) + }, + }, + { + name: "error", + msg: "error message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Error(msg) + }, + }, + } + + for _, tst := range testsOneShotLogs { + t.Run(tst.name, func(t *testing.T) { + logger, targetLogger := testCreateSLogLogger("", true) + + tst.writeLog(logger, tst.msg) + + assertSimpleMessage(t, targetLogger, tst.msg, true) + }) + } + }) + + t.Run("with debug and prefix", func(t *testing.T) { + testsOneShotLogs := []test{ + { + name: "debug", + msg: "debug message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Debug(msg) + }, + }, + + { + name: "info", + msg: "info message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Info(msg) + }, + }, + { + name: "warning", + msg: "warn message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Warn(msg) + }, + }, + { + name: "error", + msg: "error message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Error(msg) + }, + }, + } + + for _, tst := range testsOneShotLogs { + t.Run(tst.name, func(t *testing.T) { + const prefix = "ssh" + + logger, targetLogger := testCreateSLogLogger(prefix, true) + + tst.writeLog(logger, tst.msg) + + assertSimpleMessage(t, targetLogger, fmt.Sprintf(`%s: %s`, prefix, tst.msg), true) + }) + } + }) + + t.Run("without debug but with prefix", func(t *testing.T) { + testsInLog := []test{ + { + name: "info", + msg: "info message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Info(msg) + }, + }, + { + name: "warning", + msg: "warn message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Warn(msg) + }, + }, + { + name: "error", + msg: "error message", + writeLog: func(logger *slog.Logger, msg string) { + logger.Error(msg) + }, + }, + } + + logger, targetLogger := testCreateSLogLogger("ssh", false) + + for _, tst := range testsInLog { + t.Run(tst.name, func(t *testing.T) { + tst.writeLog(logger, tst.msg) + + assertSimpleMessage(t, targetLogger, fmt.Sprintf("ssh: %s", tst.msg), true) + }) + } + + t.Run("debug should not present", func(t *testing.T) { + const debugMsg = "debug message" + + logger.Debug(debugMsg) + assertSimpleMessage(t, targetLogger, debugMsg, false) + }) + }) + + t.Run("with groups", func(t *testing.T) { + const ( + firstGroup = "first-group" + secondGroup = "second-group" + thirdGroup = "second-group" + ) + + tests := []struct { + groups []string + }{ + {groups: []string{firstGroup}}, + {groups: []string{firstGroup, secondGroup}}, + {groups: []string{firstGroup, secondGroup, thirdGroup}}, + } + + for _, tst := range tests { + t.Run(strings.Join(tst.groups, "_"), func(t *testing.T) { + logger, targetLogger := testCreateSLogLogger("", false) + for _, group := range tst.groups { + logger = logger.WithGroup(group) + } + + const msg = "some message" + logger.Info(msg) + + expectedMsg := fmt.Sprintf(`%s | groups: '%s'`, msg, strings.Join(tst.groups, "/")) + + assertSimpleMessage(t, targetLogger, expectedMsg, true) + }) + } + }) + + t.Run("with attributes", func(t *testing.T) { + tests := []struct { + name string + attrs []any + attrsSuffix string + }{ + { + name: "with one attribute", + attrs: []any{"key", "value"}, + attrsSuffix: "[key='value']", + }, + + { + name: "with multiple attributes with different kinds", + attrs: []any{"key", "value with space", "err", fmt.Errorf("error"), "int", 42}, + attrsSuffix: "[key='value with space' err='error' int='42']", + }, + } + + for _, tst := range tests { + t.Run(tst.name, func(t *testing.T) { + logger, targetLogger := testCreateSLogLogger("", false) + logger = logger.With(tst.attrs...) + + const msg = "some message" + logger.Info(msg) + + expectedMsg := fmt.Sprintf(`%s | attributes: %s`, msg, tst.attrsSuffix) + + assertSimpleMessage(t, targetLogger, expectedMsg, true) + }) + } + }) + + t.Run("all in", func(t *testing.T) { + logger, targetLogger := testCreateSLogLogger("ssh", true) + + logger = logger.With("key", "value with spaces").WithGroup("my-group") + + logger.Debug("my message") + + expectedMsg := `ssh: my message | groups: 'my-group' | attributes: [key='value with spaces']` + assertSimpleMessage(t, targetLogger, expectedMsg, true) + }) +} + +func testCreateSLogLogger(prefix string, isDebug bool) (*slog.Logger, *InMemoryLogger) { + parentLogger := NewInMemoryLoggerWithParent(NewSimpleLogger(LoggerOptions{IsDebug: isDebug})) + provider := SimpleLoggerProvider(parentLogger) + return NewSLogWithPrefixAndDebug(context.TODO(), provider, prefix, isDebug), parentLogger +} diff --git a/pkg/log/tee.go b/pkg/log/tee.go index 0effd87..e0766aa 100644 --- a/pkg/log/tee.go +++ b/pkg/log/tee.go @@ -85,7 +85,7 @@ func (d *TeeLogger) FlushAndClose() error { err := d.buf.Flush() if err != nil { - d.l.WarnF("Cannot flush TeeLogger: %v \n", err) + d.l.WarnF("Cannot flush TeeLogger: %v", err) return err } @@ -93,7 +93,7 @@ func (d *TeeLogger) FlushAndClose() error { err = d.out.Close() if err != nil { - d.l.WarnF("Cannot close TeeLogger file: %v \n", err) + d.l.WarnF("Cannot close TeeLogger file: %v", err) return err } @@ -119,8 +119,8 @@ func (d *TeeLogger) Process(p Process, t string, run func() error) error { return err } -func (d *TeeLogger) InfoF(format string, a ...interface{}) { - d.l.InfoF(format, a...) +func (d *TeeLogger) InfoFWithoutLn(format string, a ...interface{}) { + d.l.InfoFWithoutLn(format, a...) d.writeToFile(fmt.Sprintf(format, a...)) } @@ -131,8 +131,8 @@ func (d *TeeLogger) InfoLn(a ...interface{}) { d.writeToFile(fmt.Sprintln(a...)) } -func (d *TeeLogger) ErrorF(format string, a ...interface{}) { - d.l.ErrorF(format, a...) +func (d *TeeLogger) ErrorFWithoutLn(format string, a ...interface{}) { + d.l.ErrorFWithoutLn(format, a...) d.writeToFile(fmt.Sprintf(format, a...)) } @@ -143,8 +143,8 @@ func (d *TeeLogger) ErrorLn(a ...interface{}) { d.writeToFile(fmt.Sprintln(a...)) } -func (d *TeeLogger) DebugF(format string, a ...interface{}) { - d.l.DebugF(format, a...) +func (d *TeeLogger) DebugFWithoutLn(format string, a ...interface{}) { + d.l.DebugFWithoutLn(format, a...) d.writeToFile(fmt.Sprintf(format, a...)) } @@ -179,8 +179,8 @@ func (d *TeeLogger) WarnLn(a ...interface{}) { d.writeToFile(fmt.Sprintln(a...)) } -func (d *TeeLogger) WarnF(format string, a ...interface{}) { - d.l.WarnF(format, a...) +func (d *TeeLogger) WarnFWithoutLn(format string, a ...interface{}) { + d.l.WarnFWithoutLn(format, a...) d.writeToFile(fmt.Sprintf(format, a...)) } diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go index 08ccf05..0f663ff 100644 --- a/pkg/retry/retry.go +++ b/pkg/retry/retry.go @@ -329,7 +329,7 @@ func (l *Loop) run(ctx context.Context, task func() error) error { } if l.breakPredicate != nil && l.breakPredicate(err) { - l.logger.DebugFLn(l.prefix+"Client break loop with %v", err) + l.logger.DebugF(l.prefix+"Client break loop with %v", err) return err }