Skip to content

[duplicate-code] Duplicate Code Pattern: Global Logging Function Repetition #800

@github-actions

Description

@github-actions

🔍 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, LogDebug
    • internal/logger/markdown_logger.go (lines 187-204): LogInfoMd, LogWarnMd, LogErrorMd, LogDebugMd
    • internal/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 consistency

Benefits:

  • 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-finished to 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions