summaryrefslogtreecommitdiff
path: root/internal/logging/logging.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-16 03:14:46 +0200
committerPaul Buetow <paul@buetow.org>2026-03-16 03:14:46 +0200
commita71136dc8c2a51dcaa49e4af5e42b3c6bffd6fa0 (patch)
treeed05684846c4178a0003d0324645ea8dc49978d4 /internal/logging/logging.go
parent1fc1611fa99993cab5dc8bf0844183285296e3b2 (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.go37
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
}