fix(log): harden logger defaults and default proxy safety

This commit is contained in:
Virgil 2026-03-29 22:34:20 +00:00
parent 08b2a70fc5
commit 3ed9ea71dc
2 changed files with 117 additions and 48 deletions

View file

@ -237,7 +237,7 @@ func LogError(err error, op, msg string) error {
return nil
}
wrapped := Wrap(err, op, msg)
defaultLogger.Error(msg, "op", op, "err", err)
Default().Error(msg, "op", op, "err", wrapped)
return wrapped
}
@ -252,7 +252,7 @@ func LogWarn(err error, op, msg string) error {
return nil
}
wrapped := Wrap(err, op, msg)
defaultLogger.Warn(msg, "op", op, "err", err)
Default().Warn(msg, "op", op, "err", wrapped)
return wrapped
}
@ -264,7 +264,8 @@ func LogWarn(err error, op, msg string) error {
// log.Must(Initialize(), "app", "startup failed")
func Must(err error, op, msg string) {
if err != nil {
defaultLogger.Error(msg, "op", op, "err", err)
panic(Wrap(err, op, msg))
wrapped := Wrap(err, op, msg)
Default().Error(msg, "op", op, "err", wrapped)
panic(wrapped)
}
}

156
log.go
View file

@ -11,6 +11,7 @@ import (
"os"
"os/user"
"slices"
"strings"
"sync"
"time"
)
@ -32,6 +33,12 @@ const (
LevelDebug
)
const (
defaultRotationMaxSize = 100
defaultRotationMaxAge = 28
defaultRotationMaxBackups = 5
)
// String returns the level name.
func (l Level) String() string {
switch l {
@ -109,16 +116,21 @@ 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
}
output := opts.Output
if opts.Rotation != nil && opts.Rotation.Filename != "" && RotationWriterFactory != nil {
output = RotationWriterFactory(*opts.Rotation)
output = RotationWriterFactory(normaliseRotationOptions(*opts.Rotation))
}
if output == nil {
output = os.Stderr
}
return &Logger{
level: opts.Level,
level: level,
output: output,
redactKeys: slices.Clone(opts.RedactKeys),
StyleTimestamp: identity,
@ -130,8 +142,28 @@ func New(opts Options) *Logger {
}
}
func normaliseRotationOptions(opts RotationOptions) RotationOptions {
if opts.MaxSize <= 0 {
opts.MaxSize = defaultRotationMaxSize
}
if opts.MaxAge == 0 {
opts.MaxAge = defaultRotationMaxAge
}
if opts.MaxBackups <= 0 {
opts.MaxBackups = defaultRotationMaxBackups
}
return opts
}
func identity(s string) string { return s }
func safeStyle(style func(string) string) func(string) string {
if style == nil {
return identity
}
return style
}
// SetLevel changes the log level.
func (l *Logger) SetLevel(level Level) {
l.mu.Lock()
@ -148,6 +180,9 @@ func (l *Logger) Level() Level {
// SetOutput changes the output writer.
func (l *Logger) SetOutput(w goio.Writer) {
if w == nil {
w = os.Stderr
}
l.mu.Lock()
l.output = w
l.mu.Unlock()
@ -167,45 +202,46 @@ func (l *Logger) shouldLog(level Level) bool {
}
func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) {
_ = level
l.mu.RLock()
output := l.output
styleTimestamp := l.StyleTimestamp
redactKeys := l.redactKeys
l.mu.RUnlock()
if styleTimestamp == nil {
styleTimestamp = identity
}
timestamp := styleTimestamp(time.Now().Format("15:04:05"))
existing := make(map[string]struct{}, len(keyvals)/2+2)
for i := 0; i < len(keyvals); i += 2 {
if key, ok := keyvals[i].(string); ok {
existing[key] = struct{}{}
}
}
// Automatically extract context from error if present in keyvals
origLen := len(keyvals)
for i := 0; i < origLen; i += 2 {
if i+1 < origLen {
if err, ok := keyvals[i+1].(error); ok {
if op := Op(err); op != "" {
// Check if op is already in keyvals
hasOp := false
for j := 0; j < len(keyvals); j += 2 {
if k, ok := keyvals[j].(string); ok && k == "op" {
hasOp = true
break
}
}
if !hasOp {
keyvals = append(keyvals, "op", op)
}
}
if stack := FormatStackTrace(err); stack != "" {
// Check if stack is already in keyvals
hasStack := false
for j := 0; j < len(keyvals); j += 2 {
if k, ok := keyvals[j].(string); ok && k == "stack" {
hasStack = true
break
}
}
if !hasStack {
keyvals = append(keyvals, "stack", stack)
}
}
if i+1 >= origLen {
continue
}
err, ok := keyvals[i+1].(error)
if !ok {
continue
}
if op := Op(err); op != "" {
if _, hasOp := existing["op"]; !hasOp {
existing["op"] = struct{}{}
keyvals = append(keyvals, "op", op)
}
}
if stack := FormatStackTrace(err); stack != "" {
if _, hasStack := existing["stack"]; !hasStack {
existing["stack"] = struct{}{}
keyvals = append(keyvals, "stack", stack)
}
}
}
@ -225,8 +261,7 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) {
}
// Redaction logic
keyStr := fmt.Sprintf("%v", key)
if slices.Contains(redactKeys, keyStr) {
if shouldRedact(key, redactKeys) {
val = "[REDACTED]"
}
@ -245,28 +280,40 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) {
// Debug logs a debug message with optional key-value pairs.
func (l *Logger) Debug(msg string, keyvals ...any) {
if l.shouldLog(LevelDebug) {
l.log(LevelDebug, l.StyleDebug("[DBG]"), msg, keyvals...)
l.mu.RLock()
style := safeStyle(l.StyleDebug)
l.mu.RUnlock()
l.log(LevelDebug, style("[DBG]"), msg, keyvals...)
}
}
// Info logs an info message with optional key-value pairs.
func (l *Logger) Info(msg string, keyvals ...any) {
if l.shouldLog(LevelInfo) {
l.log(LevelInfo, l.StyleInfo("[INF]"), msg, keyvals...)
l.mu.RLock()
style := safeStyle(l.StyleInfo)
l.mu.RUnlock()
l.log(LevelInfo, style("[INF]"), msg, keyvals...)
}
}
// Warn logs a warning message with optional key-value pairs.
func (l *Logger) Warn(msg string, keyvals ...any) {
if l.shouldLog(LevelWarn) {
l.log(LevelWarn, l.StyleWarn("[WRN]"), msg, keyvals...)
l.mu.RLock()
style := safeStyle(l.StyleWarn)
l.mu.RUnlock()
l.log(LevelWarn, style("[WRN]"), msg, keyvals...)
}
}
// Error logs an error message with optional key-value pairs.
func (l *Logger) Error(msg string, keyvals ...any) {
if l.shouldLog(LevelError) {
l.log(LevelError, l.StyleError("[ERR]"), msg, keyvals...)
l.mu.RLock()
style := safeStyle(l.StyleError)
l.mu.RUnlock()
l.log(LevelError, style("[ERR]"), msg, keyvals...)
}
}
@ -275,7 +322,10 @@ func (l *Logger) Error(msg string, keyvals ...any) {
// log configurations.
func (l *Logger) Security(msg string, keyvals ...any) {
if l.shouldLog(LevelError) {
l.log(LevelError, l.StyleSecurity("[SEC]"), msg, keyvals...)
l.mu.RLock()
style := safeStyle(l.StyleSecurity)
l.mu.RUnlock()
l.log(LevelError, style("[SEC]"), msg, keyvals...)
}
}
@ -295,48 +345,66 @@ func Username() string {
// --- Default logger ---
var defaultLogger = New(Options{Level: LevelInfo})
var defaultLoggerMu sync.RWMutex
// Default returns the default logger.
func Default() *Logger {
defaultLoggerMu.RLock()
defer defaultLoggerMu.RUnlock()
return defaultLogger
}
// SetDefault sets the default logger.
func SetDefault(l *Logger) {
if l == nil {
return
}
defaultLoggerMu.Lock()
defaultLogger = l
defaultLoggerMu.Unlock()
}
// SetLevel sets the default logger's level.
func SetLevel(level Level) {
defaultLogger.SetLevel(level)
Default().SetLevel(level)
}
// SetRedactKeys sets the default logger's redaction keys.
func SetRedactKeys(keys ...string) {
defaultLogger.SetRedactKeys(keys...)
Default().SetRedactKeys(keys...)
}
// Debug logs to the default logger.
func Debug(msg string, keyvals ...any) {
defaultLogger.Debug(msg, keyvals...)
Default().Debug(msg, keyvals...)
}
// Info logs to the default logger.
func Info(msg string, keyvals ...any) {
defaultLogger.Info(msg, keyvals...)
Default().Info(msg, keyvals...)
}
// Warn logs to the default logger.
func Warn(msg string, keyvals ...any) {
defaultLogger.Warn(msg, keyvals...)
Default().Warn(msg, keyvals...)
}
// Error logs to the default logger.
func Error(msg string, keyvals ...any) {
defaultLogger.Error(msg, keyvals...)
Default().Error(msg, keyvals...)
}
// Security logs to the default logger.
func Security(msg string, keyvals ...any) {
defaultLogger.Security(msg, keyvals...)
Default().Security(msg, keyvals...)
}
func shouldRedact(key any, redactKeys []string) bool {
keyStr := fmt.Sprintf("%v", key)
for _, redactKey := range redactKeys {
if strings.EqualFold(redactKey, keyStr) {
return true
}
}
return false
}