refactor(ax): complete logger docs and safe-default regression coverage
This commit is contained in:
parent
6b6f025be7
commit
3962cb7ac3
2 changed files with 114 additions and 0 deletions
9
log.go
9
log.go
|
|
@ -128,6 +128,12 @@ type Options struct {
|
|||
var RotationWriterFactory func(RotationOptions) goio.WriteCloser
|
||||
|
||||
// New creates a new Logger with the given options.
|
||||
//
|
||||
// logger := log.New(log.Options{
|
||||
// Level: log.LevelInfo,
|
||||
// Output: os.Stdout,
|
||||
// RedactKeys: []string{"password", "token"},
|
||||
// })
|
||||
func New(opts Options) *Logger {
|
||||
level := normaliseLevel(opts.Level)
|
||||
|
||||
|
|
@ -359,6 +365,8 @@ func (l *Logger) Security(msg string, keyvals ...any) {
|
|||
|
||||
// Username returns the current system username.
|
||||
// It uses os/user for reliability and falls back to environment variables.
|
||||
//
|
||||
// user := log.Username()
|
||||
func Username() string {
|
||||
if u, err := user.Current(); err == nil {
|
||||
return u.Username
|
||||
|
|
@ -398,6 +406,7 @@ func Default() *Logger {
|
|||
}
|
||||
|
||||
// SetDefault sets the default logger.
|
||||
// Passing nil is ignored to preserve the current default logger.
|
||||
//
|
||||
// log.SetDefault(customLogger)
|
||||
func SetDefault(l *Logger) {
|
||||
|
|
|
|||
105
log_test.go
105
log_test.go
|
|
@ -3,6 +3,7 @@ package log
|
|||
import (
|
||||
"bytes"
|
||||
goio "io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
|
@ -120,6 +121,22 @@ func TestLogger_Redaction_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLogger_Redaction_Good_CaseInsensitiveKeys(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := New(Options{
|
||||
Level: LevelInfo,
|
||||
Output: &buf,
|
||||
RedactKeys: []string{"password"},
|
||||
})
|
||||
|
||||
l.Info("login", "PASSWORD", "secret123")
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "PASSWORD=\"[REDACTED]\"") {
|
||||
t.Errorf("expected case-insensitive redaction, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogger_InjectionPrevention_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := New(Options{Level: LevelInfo, Output: &buf})
|
||||
|
|
@ -248,6 +265,17 @@ func TestLogger_SetOutput_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLogger_SetOutput_Bad_NilFallsBackToStderr(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := New(Options{Level: LevelInfo, Output: &buf})
|
||||
|
||||
l.SetOutput(nil)
|
||||
|
||||
if l.output != os.Stderr {
|
||||
t.Errorf("expected nil output to fallback to os.Stderr, got %T", l.output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogger_SetRedactKeys_Good(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := New(Options{Level: LevelInfo, Output: &buf})
|
||||
|
|
@ -333,6 +361,32 @@ func TestNew_RotationFactory_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNew_RotationFactory_Good_DefaultRetentionValues(t *testing.T) {
|
||||
original := RotationWriterFactory
|
||||
defer func() { RotationWriterFactory = original }()
|
||||
|
||||
var captured RotationOptions
|
||||
RotationWriterFactory = func(opts RotationOptions) goio.WriteCloser {
|
||||
captured = opts
|
||||
return nopWriteCloser{goio.Discard}
|
||||
}
|
||||
|
||||
_ = New(Options{
|
||||
Level: LevelInfo,
|
||||
Rotation: &RotationOptions{Filename: "test.log"},
|
||||
})
|
||||
|
||||
if captured.MaxSize != defaultRotationMaxSize {
|
||||
t.Errorf("expected default MaxSize=%d, got %d", defaultRotationMaxSize, captured.MaxSize)
|
||||
}
|
||||
if captured.MaxAge != defaultRotationMaxAge {
|
||||
t.Errorf("expected default MaxAge=%d, got %d", defaultRotationMaxAge, captured.MaxAge)
|
||||
}
|
||||
if captured.MaxBackups != defaultRotationMaxBackups {
|
||||
t.Errorf("expected default MaxBackups=%d, got %d", defaultRotationMaxBackups, captured.MaxBackups)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_DefaultOutput_Good(t *testing.T) {
|
||||
// No output or rotation — should default to stderr (not nil)
|
||||
l := New(Options{Level: LevelInfo})
|
||||
|
|
@ -341,6 +395,13 @@ func TestNew_DefaultOutput_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNew_Bad_InvalidLevelDefaultsToInfo(t *testing.T) {
|
||||
l := New(Options{Level: Level(99)})
|
||||
if l.Level() != LevelInfo {
|
||||
t.Errorf("expected invalid level to default to info, got %v", l.Level())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsername_Good(t *testing.T) {
|
||||
name := Username()
|
||||
if name == "" {
|
||||
|
|
@ -379,3 +440,47 @@ func TestDefault_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefault_Bad_SetDefaultNilIgnored(t *testing.T) {
|
||||
original := Default()
|
||||
var buf bytes.Buffer
|
||||
custom := New(Options{Level: LevelInfo, Output: &buf})
|
||||
SetDefault(custom)
|
||||
defer SetDefault(original)
|
||||
|
||||
SetDefault(nil)
|
||||
|
||||
if Default() != custom {
|
||||
t.Error("expected SetDefault(nil) to preserve the current default logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogger_StyleHooks_Bad_NilHooksDoNotPanic(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
l := New(Options{Level: LevelDebug, Output: &buf})
|
||||
l.StyleTimestamp = nil
|
||||
l.StyleDebug = nil
|
||||
l.StyleInfo = nil
|
||||
l.StyleWarn = nil
|
||||
l.StyleError = nil
|
||||
l.StyleSecurity = nil
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("expected nil style hooks not to panic, got panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
l.Debug("debug")
|
||||
l.Info("info")
|
||||
l.Warn("warn")
|
||||
l.Error("error")
|
||||
l.Security("security")
|
||||
|
||||
output := buf.String()
|
||||
for _, tag := range []string{"[DBG]", "[INF]", "[WRN]", "[ERR]", "[SEC]"} {
|
||||
if !strings.Contains(output, tag) {
|
||||
t.Errorf("expected %s in output, got %q", tag, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue