fix(log): normalise levels, preserve nested error codes, and sanitise log messages
This commit is contained in:
parent
3ed9ea71dc
commit
355af66a5c
4 changed files with 58 additions and 13 deletions
10
errors.go
10
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
31
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 ---
|
||||
|
|
|
|||
22
log_test.go
22
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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue