fix(log): normalise levels, preserve nested error codes, and sanitise log messages
Some checks failed
CI / auto-fix (push) Failing after 0s
CI / test (push) Failing after 3s
CI / auto-merge (push) Failing after 0s

This commit is contained in:
Virgil 2026-03-29 22:39:46 +00:00
parent 3ed9ea71dc
commit 355af66a5c
4 changed files with 58 additions and 13 deletions

View file

@ -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
}

View file

@ -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)

31
log.go
View file

@ -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 ---

View file

@ -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) {