Skip to content

[duplicate-code] Duplicate Code Pattern: Logger Close() Method Duplication #438

Description

@github-actions

🔍 Duplicate Code Pattern: Logger Close() Method Duplication

Part of duplicate code analysis: #435

Summary

The three logger types implement nearly identical Close() methods that follow the same pattern: acquire mutex, call closeLogFile() helper, release mutex. While the duplication is minimal (6-8 lines per method), it represents an opportunity for interface-based abstraction.

Duplication Details

Pattern: Logger Close() Methods

  • Severity: Medium
  • Occurrences: 3 instances (FileLogger, JSONLLogger, MarkdownLogger)
  • Locations:
    • internal/logger/file_logger.go (lines 58-62, 5 lines)
    • internal/logger/jsonl_logger.go (lines 59-63, 5 lines)
    • internal/logger/markdown_logger.go (lines 67-80, 14 lines)

Code Comparison:

FileLogger.Close() (5 lines):

func (fl *FileLogger) Close() error {
    fl.mu.Lock()
    defer fl.mu.Unlock()
    return closeLogFile(fl.logFile, &fl.mu, "file")
}

JSONLLogger.Close() (5 lines):

func (jl *JSONLLogger) Close() error {
    jl.mu.Lock()
    defer jl.mu.Unlock()
    return closeLogFile(jl.logFile, &jl.mu, "JSONL")
}

MarkdownLogger.Close() (14 lines - more complex):

func (ml *MarkdownLogger) Close() error {
    ml.mu.Lock()
    defer ml.mu.Unlock()

    if ml.logFile != nil {
        // Write closing details tag before closing
        footer := "\n</details>\n"
        if _, err := ml.logFile.WriteString(footer); err != nil {
            // Even if footer write fails, try to close the file properly
            return closeLogFile(ml.logFile, &ml.mu, "markdown")
        }

        // Footer written successfully, now close
        return closeLogFile(ml.logFile, &ml.mu, "markdown")
    }
    return nil
}

Note: MarkdownLogger is an exception - it needs special cleanup logic to write the footer before closing.

Impact Analysis

Maintainability

  • Low impact: Only 3 instances, pattern is clear and well-established
  • Common pattern in Go: Wrapper methods around shared helpers are idiomatic
  • Already refactored: The closeLogFile() helper in common.go (lines 23-36) consolidates the actual closing logic

Bug Risk

  • Very low: The pattern is simple and unlikely to diverge
  • Good design: Each logger calls the same closeLogFile() helper, ensuring consistent behavior
  • Special cases handled: MarkdownLogger's footer-writing logic is appropriately placed in its Close() method

Code Bloat

  • Minimal: ~10 lines total duplication (excluding MarkdownLogger's special logic)
  • Not a priority: Given the small size and clear pattern, this is acceptable duplication

Refactoring Recommendations

Option 1: Interface with Default Implementation (Go 1.23+)

Note: This requires Go 1.23+ for interface with methods. Consider carefully before implementing.

// Define a closable logger interface
type ClosableLogger interface {
    GetLogFile() *os.File
    GetMutex() *sync.Mutex
    GetLoggerName() string
    BeforeClose() error  // Hook for custom cleanup
}

// Default Close implementation
func CloseLogger(cl ClosableLogger) error {
    mu := cl.GetMutex()
    mu.Lock()
    defer mu.Unlock()
    
    // Call before-close hook if needed
    if err := cl.BeforeClose(); err != nil {
        return err
    }
    
    return closeLogFile(cl.GetLogFile(), mu, cl.GetLoggerName())
}

// Usage:
func (fl *FileLogger) Close() error {
    return CloseLogger(fl)
}

func (fl *FileLogger) BeforeClose() error {
    return nil  // No special cleanup
}

func (ml *MarkdownLogger) BeforeClose() error {
    if ml.logFile != nil {
        footer := "\n</details>\n"
        _, err := ml.logFile.WriteString(footer)
        return err
    }
    return nil
}

Benefits:

  • Eliminates ~10 lines of duplication
  • Makes the close pattern explicit and reusable
  • Provides clear extension point for logger-specific cleanup

Drawbacks:

  • Adds complexity for minimal gain
  • Requires additional interface methods
  • Less idiomatic than simple wrapper methods in Go

Estimated effort: 2-3 hours (not recommended given low value)

Option 2: Accept the Pattern (Recommended)

Recommendation: Do not refactor - this level of duplication is acceptable and idiomatic in Go.

Rationale:

  1. Go idiom: Wrapper methods that add context to shared functions are standard practice
  2. Clear and simple: Each Close() method is trivial to understand
  3. Low maintenance burden: Unlikely to change or diverge
  4. Special cases: MarkdownLogger demonstrates why per-type methods are valuable
  5. Already optimized: The shared closeLogFile() helper already eliminates the complex logic
  6. Cost-benefit: Refactoring effort outweighs benefits

What has been done well:

  • ✅ Shared closeLogFile() helper in common.go (36 lines of reusable logic)
  • ✅ Consistent error handling across all Close() methods
  • ✅ Proper mutex locking pattern
  • ✅ Each logger can customize behavior as needed (MarkdownLogger footer)

Option 3: Document the Pattern

If concerned about consistency, add documentation:

// internal/logger/common.go

// Close Pattern for Logger Types
//
// All logger types should implement Close() using this pattern:
//
//   func (*Logger) Close() error {
//       *.mu.Lock()
//       defer *.mu.Unlock()
//       
//       // Optional: Perform cleanup before closing (e.g., write footer)
//       
//       return closeLogFile(*.logFile, &*.mu, "loggerName")
//   }
//
// The closeLogFile helper handles syncing and closing with proper error handling.

Benefits:

  • Makes the pattern explicit for future developers
  • Ensures consistency when adding new logger types
  • Zero code changes required

Estimated effort: 15 minutes

Implementation Checklist

  • Recommended: Accept this pattern as acceptable duplication
  • Optional: Add documentation to common.go explaining the Close() pattern
  • Not recommended: Implement interface-based abstraction (cost exceeds benefit)

Parent Issue

See parent analysis report: #435
Related to #435

AI generated by Duplicate Code Detector

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions