diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-16 03:14:46 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-16 03:14:46 +0200 |
| commit | a71136dc8c2a51dcaa49e4af5e42b3c6bffd6fa0 (patch) | |
| tree | ed05684846c4178a0003d0324645ea8dc49978d4 /internal/logging/logging.go | |
| parent | 1fc1611fa99993cab5dc8bf0844183285296e3b2 (diff) | |
Fix data races in logging package using atomic operations
Replace bare package-level vars with atomic.Pointer[log.Logger] for std
and atomic.Int32 for logPreviewLimit to prevent concurrent access races.
Add comprehensive tests including concurrency, edge cases, and nil logger.
Coverage: 100%.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/logging/logging.go')
| -rw-r--r-- | internal/logging/logging.go | 37 |
1 files changed, 22 insertions, 15 deletions
diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 259ad68..9d84ea3 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -1,9 +1,11 @@ // Summary: ANSI-styled logging utilities with a bound standard logger and configurable preview truncation. +// All package-level state is accessed via atomic types to avoid data races under concurrent use. package logging import ( "fmt" "log" + "sync/atomic" ) // ANSI color utilities shared across Hexai. @@ -20,35 +22,40 @@ const ( // AnsiBase is the default style: black background + grey foreground. const AnsiBase = AnsiBgBlack + AnsiGrey -// singleton logger used across the codebase -var std *log.Logger +// std is the singleton logger used across the codebase, stored atomically for concurrent safety. +var std atomic.Pointer[log.Logger] -// Bind sets the underlying standard logger to use for Logf. -func Bind(l *log.Logger) { std = l } +// Bind atomically sets the underlying standard logger to use for Logf. +func Bind(l *log.Logger) { std.Store(l) } // Logf prints a formatted message with a module prefix and base ANSI style. +// It atomically loads the bound logger; if none is set the call is a no-op. func Logf(prefix, format string, args ...any) { - if std == nil { + l := std.Load() + if l == nil { return } msg := fmt.Sprintf(format, args...) - std.Print(AnsiBase + prefix + msg + AnsiReset) + l.Print(AnsiBase + prefix + msg + AnsiReset) } -// Logging configuration for previews (shared) -var logPreviewLimit int // 0 means unlimited +// logPreviewLimit is the max characters logged for request/response previews. +// Stored as atomic.Int32 for concurrent safety; 0 means unlimited. +var logPreviewLimit atomic.Int32 -// SetLogPreviewLimit sets the maximum number of characters to log for -// request/response previews. Set to 0 for unlimited. -func SetLogPreviewLimit(n int) { logPreviewLimit = n } +// SetLogPreviewLimit atomically sets the maximum number of characters to log +// for request/response previews. Set to 0 for unlimited. +func SetLogPreviewLimit(n int) { logPreviewLimit.Store(int32(n)) } -// PreviewForLog returns the string truncated to the configured preview limit. +// PreviewForLog returns the string truncated to the configured preview limit +// (loaded atomically). func PreviewForLog(s string) string { - if logPreviewLimit > 0 { - if len(s) <= logPreviewLimit { + limit := int(logPreviewLimit.Load()) + if limit > 0 { + if len(s) <= limit { return s } - return s[:logPreviewLimit] + "…" + return s[:limit] + "…" } return s } |
