refactor(ax): complete logger docs and safe-default regression coverage
Some checks failed
CI / test (push) Failing after 2s
CI / auto-fix (push) Failing after 1s
CI / auto-merge (push) Failing after 1s

This commit is contained in:
Virgil 2026-03-30 05:27:49 +00:00
parent 6b6f025be7
commit 3962cb7ac3
2 changed files with 114 additions and 0 deletions

9
log.go
View file

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

View file

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