-
Notifications
You must be signed in to change notification settings - Fork 3
Description
🔍 Duplicate Code Pattern: Global Logging Function Repetition
Part of duplicate code analysis: #797
Summary
The logger package contains repetitive implementations of global logging wrapper functions (LogInfo, LogWarn, LogError, LogDebug) across three different logger files. Each file implements nearly identical boilerplate for thread-safe access to global logger instances, resulting in ~96+ lines of duplicated code.
Duplication Details
Pattern: Global Logging Function Wrappers
- Severity: Medium
- Occurrences: 3 sets of 4 functions each (12 functions total)
- Locations:
internal/logger/file_logger.go(lines 114-151): LogInfo, LogWarn, LogError, LogDebuginternal/logger/markdown_logger.go(lines 187-204): LogInfoMd, LogWarnMd, LogErrorMd, LogDebugMdinternal/logger/server_file_logger.go(lines 162-199): LogInfoWithServer, LogWarnWithServer, LogErrorWithServer, LogDebugWithServer
Duplicated Structure
Each file implements 4 nearly identical wrapper functions:
// Pattern repeated 12 times (4 functions × 3 files)
func Log(Level)[Variant](category, format string, args ...interface{}) {
global(Logger)Mu.RLock()
defer global(Logger)Mu.RUnlock()
if global(Logger) != nil {
global(Logger).Log(LogLevel(Level), category, format, args...)
}
}Code Examples
File Logger (file_logger.go):
// LogInfo logs an informational message
func LogInfo(category, format string, args ...interface{}) {
globalLoggerMu.RLock()
defer globalLoggerMu.RUnlock()
if globalFileLogger != nil {
globalFileLogger.Log(LogLevelInfo, category, format, args...)
}
}
// LogWarn logs a warning message
func LogWarn(category, format string, args ...interface{}) {
globalLoggerMu.RLock()
defer globalLoggerMu.RUnlock()
if globalFileLogger != nil {
globalFileLogger.Log(LogLevelWarn, category, format, args...)
}
}
// LogError logs an error message
func LogError(category, format string, args ...interface{}) {
globalLoggerMu.RLock()
defer globalLoggerMu.RUnlock()
if globalFileLogger != nil {
globalFileLogger.Log(LogLevelError, category, format, args...)
}
}
// LogDebug logs a debug message
func LogDebug(category, format string, args ...interface{}) {
globalLoggerMu.RLock()
defer globalLoggerMu.RUnlock()
if globalFileLogger != nil {
globalFileLogger.Log(LogLevelDebug, category, format, args...)
}
}Markdown Logger (markdown_logger.go) - Nearly identical:
// LogInfoMd logs to both regular and markdown loggers
func LogInfoMd(category, format string, args ...interface{}) {
logWithMarkdown(LogLevelInfo, LogInfo, category, format, args...)
}
// LogWarnMd logs to both regular and markdown loggers
func LogWarnMd(category, format string, args ...interface{}) {
logWithMarkdown(LogLevelWarn, LogWarn, category, format, args...)
}
// LogErrorMd logs to both regular and markdown loggers
func LogErrorMd(category, format string, args ...interface{}) {
logWithMarkdown(LogLevelError, LogError, category, format, args...)
}
// LogDebugMd logs to both regular and markdown loggers
func LogDebugMd(category, format string, args ...interface{}) {
logWithMarkdown(LogLevelDebug, LogDebug, category, format, args...)
}Server File Logger (server_file_logger.go) - Same pattern with extra parameter:
// LogInfoWithServer logs to both server-specific and unified logs
func LogInfoWithServer(serverID, category, format string, args ...interface{}) {
logWithServer(LogLevelInfo, serverID, category, format, args...)
}
// LogWarnWithServer logs to both server-specific and unified logs
func LogWarnWithServer(serverID, category, format string, args ...interface{}) {
logWithServer(LogLevelWarn, serverID, category, format, args...)
}
// LogErrorWithServer logs to both server-specific and unified logs
func LogErrorWithServer(serverID, category, format string, args ...interface{}) {
logWithServer(LogLevelError, serverID, category, format, args...)
}
// LogDebugWithServer logs to both server-specific and unified logs
func LogDebugWithServer(serverID, category, format string, args ...interface{}) {
logWithServer(LogLevelDebug, serverID, category, format, args...)
}Impact Analysis
Maintainability
- Medium Risk: Changes to logging behavior require updates in 3 files
- Consistency: Each set follows the same pattern but with variations (Md suffix, WithServer suffix)
- Boilerplate: 12 functions × ~8 lines = ~96 lines of nearly identical code
Bug Risk
- Low-Medium Risk: Inconsistent error handling or nil checks across implementations
- Thread Safety: All use same RLock pattern, but easy to miss in future additions
- Null Checks: All check for nil logger, but implementation could diverge
Code Bloat
- Total Lines: ~96 lines of wrapper functions
- Percentage: ~13% of logger package code (727 total lines across core logger files)
- Scalability: Adding a new log level would require 3 new functions
Refactoring Recommendations
1. Use Function Composition with Generics
Effort: Medium (3-4 hours)
Create a generic wrapper generator to eliminate repetition:
// logWithGlobalLogger is a generic helper for global logger access
func logWithGlobalLogger[T closableLogger](
mu *sync.RWMutex,
logger *T,
level LogLevel,
logFunc func(T, LogLevel, string, string, ...interface{}),
category, format string,
args ...interface{},
) {
mu.RLock()
defer mu.RUnlock()
if *logger != nil {
logFunc(*logger, level, category, format, args...)
}
}
// Simplified wrappers (from 8 lines to 1 line each)
func LogInfo(category, format string, args ...interface{}) {
logWithGlobalLogger(&globalLoggerMu, &globalFileLogger, LogLevelInfo,
func(l *FileLogger, level LogLevel, cat, fmt string, args ...interface{}) {
l.Log(level, cat, fmt, args...)
}, category, format, args...)
}Benefits:
- Reduces each wrapper from ~8 lines to ~1-2 lines
- Centralized mutex handling and nil checking
- Type-safe with generics
- Consistent behavior across all loggers
2. Generate Wrappers with Code Generation
Effort: Medium (4-5 hours)
Use go generate to create wrapper functions from templates:
//go:generate go run generate_log_wrappers.go
// generate_log_wrappers.go
// Generates LogInfo, LogWarn, LogError, LogDebug for each logger type
// Template-based generation ensures consistencyBenefits:
- Zero manual duplication
- Easy to add new log levels or logger types
- Generated code is consistent and correct
- Clear separation between template and implementation
3. Simplify with Method Value Pattern
Effort: Low-Medium (2-3 hours)
Leverage Go's method value pattern for simpler wrappers:
// LoggerFunc defines the signature for logging functions
type LoggerFunc func(LogLevel, string, string, ...interface{})
// createGlobalLogFunc creates a thread-safe wrapper for a logger method
func createGlobalLogFunc(mu *sync.RWMutex, loggerGetter func() LoggerFunc) func(string, string, ...interface{}) {
return func(category, format string, args ...interface{}) {
mu.RLock()
defer mu.RUnlock()
if logFunc := loggerGetter(); logFunc != nil {
logFunc(category, format, args...)
}
}
}
// Usage - Define once, reuse for all levels
var (
LogInfo = createGlobalLogFunc(&globalLoggerMu, getFileLoggerFunc(LogLevelInfo))
LogWarn = createGlobalLogFunc(&globalLoggerMu, getFileLoggerFunc(LogLevelWarn))
LogError = createGlobalLogFunc(&globalLoggerMu, getFileLoggerFunc(LogLevelError))
LogDebug = createGlobalLogFunc(&globalLoggerMu, getFileLoggerFunc(LogLevelDebug))
)Benefits:
- DRY principle applied - single wrapper generator
- All 12 functions become variable declarations
- Reduces code by ~85 lines
- Maintains thread safety and nil checking
Implementation Checklist
- Choose refactoring approach (recommend Option 3 for simplicity)
- Create generic wrapper generator in
global_helpers.go - Refactor FileLogger wrappers (LogInfo, LogWarn, LogError, LogDebug)
- Refactor MarkdownLogger wrappers (LogInfoMd, LogWarnMd, LogErrorMd, LogDebugMd)
- Refactor ServerFileLogger wrappers (LogInfoWithServer, etc.)
- Update tests to verify consistent behavior
- Verify no functionality changes (all callers work identically)
- Update documentation for new wrapper pattern
- Run
make agent-finishedto verify all checks pass
Alternative: Keep Current Pattern
Rationale: The current duplication is intentional for:
- Clear, explicit function signatures for godoc
- Easy to understand for contributors
- Low change frequency (rarely modified)
- IDE autocomplete works well with explicit functions
If keeping current pattern:
- Document the pattern in logger package documentation
- Create template/checklist for adding new logger types
- Ensure consistency through code review
Parent Issue
See parent analysis report: #797
Related to #797
AI generated by Duplicate Code Detector
- expires on Feb 14, 2026, 10:13 AM UTC