diff --git a/errors.go b/errors.go index 88a0c9f..266a8dd 100644 --- a/errors.go +++ b/errors.go @@ -102,24 +102,33 @@ func NewCode(code, msg string) error { // Is reports whether any error in err's tree matches target. // Wrapper around errors.Is for convenience. +// +// if log.Is(err, context.DeadlineExceeded) { /* handle timeout */ } func Is(err, target error) bool { return errors.Is(err, target) } // As finds the first error in err's tree that matches target. // Wrapper around errors.As for convenience. +// +// var e *log.Err +// if log.As(err, &e) { /* use e.Code */ } func As(err error, target any) bool { return errors.As(err, target) } // NewError creates a simple error with the given text. // Wrapper around errors.New for convenience. +// +// return log.NewError("invalid state") func NewError(text string) error { return errors.New(text) } // Join combines multiple errors into one. // Wrapper around errors.Join for convenience. +// +// return log.Join(validateErr, persistErr) func Join(errs ...error) error { return errors.Join(errs...) } @@ -152,6 +161,8 @@ func ErrCode(err error) string { // Message extracts the message from an error. // Returns the error's Error() string if not an *Err. +// +// msg := log.Message(err) func Message(err error) string { if err == nil { return "" diff --git a/errors_test.go b/errors_test.go index 73a6202..a43d21f 100644 --- a/errors_test.go +++ b/errors_test.go @@ -336,18 +336,18 @@ func TestStackTrace_Good(t *testing.T) { assert.Equal(t, "op3 -> op2 -> op1", formatted) } -func TestStackTrace_PlainError(t *testing.T) { +func TestStackTrace_Bad_PlainError(t *testing.T) { err := errors.New("plain error") assert.Empty(t, StackTrace(err)) assert.Empty(t, FormatStackTrace(err)) } -func TestStackTrace_Nil(t *testing.T) { +func TestStackTrace_Bad_Nil(t *testing.T) { assert.Empty(t, StackTrace(nil)) assert.Empty(t, FormatStackTrace(nil)) } -func TestStackTrace_NoOp(t *testing.T) { +func TestStackTrace_Bad_NoOp(t *testing.T) { err := &Err{Msg: "no op"} assert.Empty(t, StackTrace(err)) assert.Empty(t, FormatStackTrace(err)) diff --git a/log.go b/log.go index 4f64658..5f5f4f5 100644 --- a/log.go +++ b/log.go @@ -73,13 +73,18 @@ type Logger struct { // RedactKeys is a list of keys whose values should be masked in logs. redactKeys []string - // Style functions for formatting (can be overridden) + // StyleTimestamp formats the rendered timestamp prefix. StyleTimestamp func(string) string - StyleDebug func(string) string - StyleInfo func(string) string - StyleWarn func(string) string - StyleError func(string) string - StyleSecurity func(string) string + // StyleDebug formats the debug level prefix. + StyleDebug func(string) string + // StyleInfo formats the info level prefix. + StyleInfo func(string) string + // StyleWarn formats the warning level prefix. + StyleWarn func(string) string + // StyleError formats the error level prefix. + StyleError func(string) string + // StyleSecurity formats the security event prefix. + StyleSecurity func(string) string } // RotationOptions defines the log rotation and retention policy. @@ -107,6 +112,7 @@ type RotationOptions struct { // Options configures a Logger. type Options struct { + // Level controls which messages are emitted. Level Level // Output is the destination for log messages. If Rotation is provided, // Output is ignored and logs are written to the rotating file instead. @@ -266,11 +272,11 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) { if i > 0 { kvStr += " " } - key := normaliseLogText(fmt.Sprintf("%v", keyvals[i])) - var val any - if i+1 < len(keyvals) { - val = keyvals[i+1] - } + key := normaliseLogText(fmt.Sprintf("%v", keyvals[i])) + var val any + if i+1 < len(keyvals) { + val = keyvals[i+1] + } // Redaction logic if shouldRedact(key, redactKeys) { @@ -279,12 +285,12 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) { // Secure formatting to prevent log injection if s, ok := val.(string); ok { - kvStr += fmt.Sprintf("%s=%q", key, s) - } else { - kvStr += fmt.Sprintf("%s=%v", key, normaliseLogText(fmt.Sprintf("%v", val))) + kvStr += fmt.Sprintf("%s=%q", key, s) + } else { + kvStr += fmt.Sprintf("%s=%v", key, normaliseLogText(fmt.Sprintf("%v", val))) + } } } -} _, _ = fmt.Fprintf(output, "%s %s %s%s\n", timestamp, prefix, normaliseLogText(msg), kvStr) } @@ -404,36 +410,50 @@ func SetDefault(l *Logger) { } // SetLevel sets the default logger's level. +// +// log.SetLevel(log.LevelDebug) func SetLevel(level Level) { Default().SetLevel(level) } // SetRedactKeys sets the default logger's redaction keys. +// +// log.SetRedactKeys("password", "token") func SetRedactKeys(keys ...string) { Default().SetRedactKeys(keys...) } // Debug logs to the default logger. +// +// log.Debug("query started", "sql", query) func Debug(msg string, keyvals ...any) { Default().Debug(msg, keyvals...) } // Info logs to the default logger. +// +// log.Info("server ready", "port", 8080) func Info(msg string, keyvals ...any) { Default().Info(msg, keyvals...) } // Warn logs to the default logger. +// +// log.Warn("retrying request", "attempt", 2) func Warn(msg string, keyvals ...any) { Default().Warn(msg, keyvals...) } // Error logs to the default logger. +// +// log.Error("request failed", "err", err) func Error(msg string, keyvals ...any) { Default().Error(msg, keyvals...) } // Security logs to the default logger. +// +// log.Security("suspicious login", "ip", remoteAddr) func Security(msg string, keyvals ...any) { Default().Security(msg, keyvals...) } diff --git a/log_test.go b/log_test.go index 503c854..35227bd 100644 --- a/log_test.go +++ b/log_test.go @@ -12,7 +12,7 @@ type nopWriteCloser struct{ goio.Writer } func (nopWriteCloser) Close() error { return nil } -func TestLogger_Levels(t *testing.T) { +func TestLogger_Levels_Good(t *testing.T) { tests := []struct { name string level Level @@ -62,7 +62,7 @@ func TestLogger_Levels(t *testing.T) { } } -func TestLogger_KeyValues(t *testing.T) { +func TestLogger_KeyValues_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelDebug, Output: &buf}) @@ -80,7 +80,7 @@ func TestLogger_KeyValues(t *testing.T) { } } -func TestLogger_ErrorContext(t *testing.T) { +func TestLogger_ErrorContext_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Output: &buf, Level: LevelInfo}) @@ -98,7 +98,7 @@ func TestLogger_ErrorContext(t *testing.T) { } } -func TestLogger_Redaction(t *testing.T) { +func TestLogger_Redaction_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{ Level: LevelInfo, @@ -120,7 +120,7 @@ func TestLogger_Redaction(t *testing.T) { } } -func TestLogger_InjectionPrevention(t *testing.T) { +func TestLogger_InjectionPrevention_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelInfo, Output: &buf}) @@ -174,7 +174,7 @@ func TestLogger_MessageSanitization_Good(t *testing.T) { } } -func TestLogger_SetLevel(t *testing.T) { +func TestLogger_SetLevel_Good(t *testing.T) { l := New(Options{Level: LevelInfo}) if l.Level() != LevelInfo { @@ -192,7 +192,7 @@ func TestLogger_SetLevel(t *testing.T) { } } -func TestLevel_String(t *testing.T) { +func TestLevel_String_Good(t *testing.T) { tests := []struct { level Level expected string @@ -214,7 +214,7 @@ func TestLevel_String(t *testing.T) { } } -func TestLogger_Security(t *testing.T) { +func TestLogger_Security_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{Level: LevelError, Output: &buf})