Skip to content

[duplicate-code] Duplicate Code Pattern: Logger Initialization Boilerplate #436

@github-actions

Description

@github-actions

🔍 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.Logger wrapper
  • 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 initialized flag 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions