diff --git a/errors.go b/errors.go index 7c26004..b52ee74 100644 --- a/errors.go +++ b/errors.go @@ -68,12 +68,7 @@ func Wrap(err error, op, msg string) error { if err == nil { return nil } - // Preserve Code from wrapped *Err - var logErr *Err - if As(err, &logErr) && logErr.Code != "" { - return &Err{Op: op, Msg: msg, Err: err, Code: logErr.Code} - } - return &Err{Op: op, Msg: msg, Err: err} + return &Err{Op: op, Msg: msg, Err: err, Code: ErrCode(err)} } // WrapCode wraps an error with operation context and error code. @@ -84,6 +79,9 @@ func Wrap(err error, op, msg string) error { // // return log.WrapCode(err, "VALIDATION_ERROR", "user.Validate", "invalid email") func WrapCode(err error, code, op, msg string) error { + if code == "" { + code = ErrCode(err) + } if err == nil && code == "" { return nil } diff --git a/errors_test.go b/errors_test.go index 574865e..73a6202 100644 --- a/errors_test.go +++ b/errors_test.go @@ -95,6 +95,14 @@ func TestWrap_PreservesCode_Good(t *testing.T) { assert.Contains(t, outer.Error(), "[VALIDATION_ERROR]") } +func TestWrap_PreservesCode_FromNestedChain_Good(t *testing.T) { + root := WrapCode(errors.New("base"), "CHAIN_ERROR", "inner", "inner failed") + wrapped := Wrap(fmt.Errorf("mid layer: %w", root), "outer", "outer context") + + assert.Equal(t, "CHAIN_ERROR", ErrCode(wrapped)) + assert.Contains(t, wrapped.Error(), "[CHAIN_ERROR]") +} + func TestWrap_NilError_Good(t *testing.T) { err := Wrap(nil, "op", "msg") assert.Nil(t, err) diff --git a/log.go b/log.go index 06a0c30..20546f0 100644 --- a/log.go +++ b/log.go @@ -39,6 +39,13 @@ const ( defaultRotationMaxBackups = 5 ) +func normaliseLevel(level Level) Level { + if level < LevelQuiet || level > LevelDebug { + return LevelInfo + } + return level +} + // String returns the level name. func (l Level) String() string { switch l { @@ -116,10 +123,7 @@ var RotationWriterFactory func(RotationOptions) goio.WriteCloser // New creates a new Logger with the given options. func New(opts Options) *Logger { - level := opts.Level - if level < LevelQuiet || level > LevelDebug { - level = LevelInfo - } + level := normaliseLevel(opts.Level) output := opts.Output if opts.Rotation != nil && opts.Rotation.Filename != "" && RotationWriterFactory != nil { @@ -167,7 +171,7 @@ func safeStyle(style func(string) string) func(string) string { // SetLevel changes the log level. func (l *Logger) SetLevel(level Level) { l.mu.Lock() - l.level = level + l.level = normaliseLevel(level) l.mu.Unlock() } @@ -274,7 +278,7 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) { } } - _, _ = fmt.Fprintf(output, "%s %s %s%s\n", timestamp, prefix, msg, kvStr) + _, _ = fmt.Fprintf(output, "%s %s %s%s\n", timestamp, prefix, normaliseLogText(msg), kvStr) } // Debug logs a debug message with optional key-value pairs. @@ -339,7 +343,20 @@ func Username() string { if u := os.Getenv("USER"); u != "" { return u } - return os.Getenv("USERNAME") + if u := os.Getenv("USERNAME"); u != "" { + return u + } + return "unknown" +} + +var logTextCleaner = strings.NewReplacer( + "\r", "\\r", + "\n", "\\n", + "\t", "\\t", +) + +func normaliseLogText(text string) string { + return logTextCleaner.Replace(text) } // --- Default logger --- diff --git a/log_test.go b/log_test.go index eec88f4..780b197 100644 --- a/log_test.go +++ b/log_test.go @@ -137,6 +137,23 @@ func TestLogger_InjectionPrevention(t *testing.T) { } } +func TestLogger_MessageSanitization_Good(t *testing.T) { + var buf bytes.Buffer + l := New(Options{Level: LevelInfo, Output: &buf}) + + l.Info("message\nwith\tcontrol\rchars") + output := buf.String() + + if !strings.Contains(output, "message\\nwith\\tcontrol\\rchars") { + t.Errorf("expected control characters to be escaped, got %q", output) + } + + lines := strings.Split(strings.TrimSpace(output), "\n") + if len(lines) != 1 { + t.Errorf("expected 1 line, got %d", len(lines)) + } +} + func TestLogger_SetLevel(t *testing.T) { l := New(Options{Level: LevelInfo}) @@ -148,6 +165,11 @@ func TestLogger_SetLevel(t *testing.T) { if l.Level() != LevelDebug { t.Error("expected level to be Debug after SetLevel") } + + l.SetLevel(99) + if l.Level() != LevelInfo { + t.Errorf("expected invalid level to default back to info, got %v", l.Level()) + } } func TestLevel_String(t *testing.T) {