-
Notifications
You must be signed in to change notification settings - Fork 3
Description
🔍 Duplicate Code Pattern: Logger Initialization Boilerplate
Part of duplicate code analysis: #435
Summary
The three logger initialization functions (InitFileLogger, InitJSONLLogger, InitMarkdownLogger) follow nearly identical structural patterns with only minor variations in error handling and field setup. This creates ~60-75 lines of duplicated boilerplate code across the logger package.
Duplication Details
Pattern: Logger Initialization Functions
- Severity: High
- Occurrences: 3 instances (FileLogger, JSONLLogger, MarkdownLogger)
- Locations:
internal/logger/file_logger.go(lines 30-54, 25 lines)internal/logger/jsonl_logger.go(lines 39-56, 18 lines)internal/logger/markdown_logger.go(lines 28-48, 21 lines)
Code Structure (All Three Functions):
func Init*Logger(logDir, fileName string) error {
// 1. Create logger struct
*l := &*Logger{
logDir: logDir,
fileName: fileName,
}
// 2. Call common initLogFile()
file, err := initLogFile(logDir, fileName, <FLAGS>)
if err != nil {
// 3. ERROR HANDLING (differs per logger)
// ...
}
// 4. Set logger-specific fields
*l.logFile = file
// ... additional setup
// 5. Register as global logger
initGlobal*Logger(*l)
return nil
}Specific Differences:
FileLogger (25 lines):
- Flags:
os.O_APPEND - Error handling: Fallback to stdout with warnings
- Additional setup: Creates
log.Loggerwrapper - Prints success message
JSONLLogger (18 lines):
- Flags:
os.O_APPEND - Error handling: Returns error immediately (no fallback)
- Additional setup: Creates JSON encoder
- No success message
MarkdownLogger (21 lines):
- Flags:
os.O_TRUNC - Error handling: Sets fallback flag but no stdout redirect
- Additional setup: Sets
initializedflag to false - No success message
Impact Analysis
Maintainability
- Adding a new logger: Requires copying ~20-25 lines of boilerplate with subtle variations
- Changing initialization logic: Must update 3 separate functions
- Error handling inconsistency: Different error strategies make debugging difficult
Bug Risk
- Inconsistent behavior: FileLogger falls back to stdout, JSONLLogger fails hard, MarkdownLogger fails silently
- Easy to miss: When fixing a bug in one Init function, easy to forget the other two
- Testing burden: Each Init function needs separate test cases for the same logic
Code Bloat
- ~60 lines of duplicated logic that could be reduced to ~20 lines + configuration
Refactoring Recommendations
Option 1: Functional Options Pattern (Recommended)
Extract common initialization logic with customization via options:
// Common initialization function
func initLogger[T closableLogger](
logDir, fileName string,
flags int,
setup func(*os.File) (T, error),
onError func(error) (T, error),
) (T, error) {
file, err := initLogFile(logDir, fileName, flags)
if err != nil {
return onError(err)
}
logger, err := setup(file)
if err != nil {
file.Close()
var zero T
return zero, err
}
return logger, nil
}
// Usage:
func InitFileLogger(logDir, fileName string) error {
logger, err := initLogger(
logDir, fileName, os.O_APPEND,
func(file *os.File) (*FileLogger, error) {
fl := &FileLogger{
logDir: logDir, fileName: fileName,
logFile: file,
logger: log.New(file, "", 0),
}
return fl, nil
},
func(err error) (*FileLogger, error) {
// Fallback to stdout
fl := &FileLogger{
logDir: logDir, fileName: fileName,
useFallback: true,
logger: log.New(os.Stdout, "", 0),
}
return fl, nil
},
)
initGlobalFileLogger(logger)
return err
}Benefits:
- Reduces duplication by ~40 lines
- Maintains flexibility for logger-specific behavior
- Makes error handling strategies explicit
- Easier to test common logic
Estimated effort: 2-3 hours
- Implementation: 1.5 hours
- Testing: 1 hour
- Code review: 30 minutes
Option 2: Template Method with Interfaces
Define an interface for logger initialization and use a base implementation:
type LoggerInitializer interface {
GetFlags() int
SetupLogger(*os.File) error
HandleInitError(error) error
}
func initializeLogger(init LoggerInitializer, logDir, fileName string) error {
// Common initialization logic
}Benefits:
- Traditional OOP approach, familiar pattern
- Clear separation of concerns
Drawbacks:
- More boilerplate for interface implementation
- Less idiomatic in Go compared to functional options
Estimated effort: 3-4 hours
Option 3: Configuration Struct
Use a configuration struct to parameterize initialization:
type LoggerConfig struct {
LogDir string
FileName string
Flags int
FallbackOnError bool
SetupFunc func(*os.File, *LoggerConfig) error
}
func initLoggerWithConfig(cfg *LoggerConfig) error {
// Common initialization
}Benefits:
- Simple and straightforward
- Easy to add new configuration options
Drawbacks:
- Less type-safe than functional options
- Still requires some per-logger wrapper code
Estimated effort: 2 hours
Implementation Checklist
- Review duplication findings with team
- Choose refactoring approach (recommend: Functional Options)
- Create refactoring plan with backward compatibility strategy
- Implement generic initialization function
- Migrate FileLogger to use generic function
- Migrate JSONLLogger to use generic function
- Migrate MarkdownLogger to use generic function
- Update tests for all three loggers
- Verify no functionality broken
- Update documentation if initialization API changes
Parent Issue
See parent analysis report: #435
Related to #435
AI generated by Duplicate Code Detector