Skip to content

Commit 5220bfb

Browse files
authored
Consolidate logger initialization to use generic helpers (#815)
`ServerFileLogger` was manually implementing global state management that other loggers already handled via generic helpers in `global_helpers.go`. This created 23 lines of duplicate code and made fallback behavior strategies unclear. ## Changes **Extended generic helpers to ServerFileLogger** - Added `*ServerFileLogger` to `closableLogger` constraint - Replaced manual `initGlobalServerFileLogger()` and `CloseServerFileLogger()` with calls to generic `initGlobalLogger()` and `closeGlobalLogger()` - Eliminates 23 lines of duplicate mutex handling and cleanup logic **Documented fallback strategies** Added 130-line "Initialization Pattern for Logger Types" section to `common.go` explaining when each logger uses fallbacks: - `FileLogger`: stdout fallback for critical operational logs - `MarkdownLogger`: silent fallback for optional preview logs - `JSONLLogger`: strict error mode for structured data - `ServerFileLogger`: unified logger fallback for per-server logs ## Example Before (manual implementation): ```go func initGlobalServerFileLogger(logger *ServerFileLogger) { globalServerLoggerMu.Lock() defer globalServerLoggerMu.Unlock() if globalServerFileLogger != nil { globalServerFileLogger.Close() } globalServerFileLogger = logger } ``` After (using generic helper): ```go func initGlobalServerFileLogger(logger *ServerFileLogger) { initGlobalLogger(&globalServerLoggerMu, &globalServerFileLogger, logger) } ``` All loggers now use the same thread-safe initialization pattern. Documentation provides clear guidance for adding new logger types. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build2696183341/b271/launcher.test /tmp/go-build2696183341/b271/launcher.test -test.testlogfile=/tmp/go-build2696183341/b271/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 1636032/b166/_pkg_.a go x_amd64/compile` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build2696183341/b259/config.test /tmp/go-build2696183341/b259/config.test -test.testlogfile=/tmp/go-build2696183341/b259/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 1636032/b153/_pkg_.a .cfg x_amd64/vet` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build2696183341/b271/launcher.test /tmp/go-build2696183341/b271/launcher.test -test.testlogfile=/tmp/go-build2696183341/b271/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 1636032/b166/_pkg_.a go x_amd64/compile` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build2696183341/b271/launcher.test /tmp/go-build2696183341/b271/launcher.test -test.testlogfile=/tmp/go-build2696183341/b271/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 1636032/b166/_pkg_.a go x_amd64/compile` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build2696183341/b280/mcp.test /tmp/go-build2696183341/b280/mcp.test -test.testlogfile=/tmp/go-build2696183341/b280/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.6/x64/src/runtime/c-g .cfg x_amd64/vet` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details> <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ---- *This section details on the original issue you should resolve* <issue_title>[duplicate-code] Duplicate Code Pattern: Logger Initialization Boilerplate</issue_title> <issue_description># 🔍 Duplicate Code Pattern: Logger Initialization Boilerplate *Part of duplicate code analysis: #797* ## Summary Four logger files (`file_logger.go`, `jsonl_logger.go`, `markdown_logger.go`, `server_file_logger.go`) share nearly identical initialization patterns for file handling, error management, and global state setup. This creates significant maintenance overhead and increases the risk of inconsistent behavior across loggers. ## Duplication Details ### Pattern: Logger Initialization Structure - **Severity**: High - **Occurrences**: 4 instances (FileLogger, JSONLLogger, MarkdownLogger, ServerFileLogger) - **Locations**: - `internal/logger/file_logger.go` (lines 28-60, 63-68) - `internal/logger/jsonl_logger.go` (lines 38-66, 69-74) - `internal/logger/markdown_logger.go` (lines 28-54, 72-89) - `internal/logger/server_file_logger.go` (lines 70-114) ### Duplicated Components **1. File Initialization Pattern** (~40 lines per logger) Each logger implements nearly identical logic: ```go func Init*Logger(logDir, fileName string) error { logger, err := initLogger( logDir, fileName, os.O_APPEND, // or os.O_TRUNC // Setup function func(file *os.File, logDir, fileName string) (*Logger, error) { // Create logger struct // Initialize fields // Return logger }, // Error handler func(err error, logDir, fileName string) (*Logger, error) { // Fallback logic or error return }, ) // Initialize global logger return err } ``` **2. Close Method Pattern** (~8 lines per logger) ```go func (l *Logger) Close() error { l.mu.Lock() defer l.mu.Unlock() return closeLogFile(l.logFile, &l.mu, "loggerType") } ``` **3. Global Logger State Pattern** (~12 lines per logger) ```go var ( global*Logger **Logger global*Mu sync.RWMutex ) func initGlobal*Logger(logger **Logger) { initGlobalLogger(&global*Mu, &global*Logger, logger) } func closeGlobal*Logger() error { return closeGlobalLogger(&global*Mu, &global*Logger) } ``` ### Code Sample: File Logger ```go // InitFileLogger initializes the global file logger func InitFileLogger(logDir, fileName string) error { logger, err := initLogger( logDir, fileName, os.O_APPEND, func(file *os.File, logDir, fileName string) (*FileLogger, error) { fl := &FileLogger{ logDir: logDir, fileName: fileName, logFile: file, logger: log.New(file, "", 0), } log.Printf("Logging to file: %s", filepath.Join(logDir, fileName)) return fl, nil }, func(err error, logDir, fileName string) (*FileLogger, error) { // Fallback to stdout... return fl, nil }, ) initGlobalFileLogger(logger) return err } ``` Similar structures exist in all 4 logger files with only minor variations. ## Impact Analysis ### Maintainability - **High Risk**: Changes to initialization logic must be replicated across 4 files - **Inconsistency**: Different error handling strategies across loggers (some fallback to stdout, others return errors) - **Code Bloat**: ~120+ lines of similar initialization code ### Bug Risk - **Medium Risk**: Bug fixes must be applied to all 4 loggers independently - **Historical Issues**: The codebase already recognized this pattern and created `common.go` with `initLogger()` helper, but only partial consolidation occurred - **Fallback Inconsistency**: FileLogger/MarkdownLogger fallback to stdout, JSONLLogger doesn't - easy to miss ### Code Bloat - **Total Lines**: ~120+ lines of duplicated initialization patterns - **Percentage**: ~16% of logger package code (727 total lines across 4 files) ## Refactoring Recommendations ### 1. Extract Logger Factory Pattern **Effort**: Medium (4-6 hours) Create a generic logger factory that handles common initialization: ```go // LoggerConfig defines common logger configuration type LoggerConfig struct { LogDir string FileName string FileFlags int // os.O_APPEND, os.O_TRUNC, etc. AllowFallback bool // Whether to fallback to stdout on error } // LoggerFactory creates and initializes loggers with common patterns type LoggerFactory[T closableLogger] struct { config LoggerConfig } func (f *LoggerFactory[T]) Create( setupFunc func(*os.File, string, string) (T, error), fallbackFunc func(error, string, string) (T, error), ) (T, error) { // Common initialization logic // Replaces duplicate initLogger calls } ``` **Benefits**: - Centralized file handling and error management - Consistent fallback behavior - Reduces initialization code by ~80 lines - Single source of trut... </details> > **Custom agent used: agentic-workflows** > GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #798 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.
2 parents c6255ce + c7147d6 commit 5220bfb

3 files changed

Lines changed: 143 additions & 24 deletions

File tree

internal/logger/common.go

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,128 @@ import (
7272
//
7373
// When adding a new logger type, follow this pattern to ensure consistent behavior.
7474

75+
// Initialization Pattern for Logger Types
76+
//
77+
// The logger package follows a consistent initialization pattern across all logger types,
78+
// with support for customizable fallback behavior. This section documents the pattern
79+
// and explains the different fallback strategies used by each logger type.
80+
//
81+
// Standard Initialization Pattern:
82+
//
83+
// All logger types use the initLogger() generic helper function for initialization:
84+
//
85+
// func Init*Logger(logDir, fileName string) error {
86+
// logger, err := initLogger(
87+
// logDir, fileName, fileFlags,
88+
// setupFunc, // Configure logger after file is opened
89+
// errorHandler, // Handle initialization failures
90+
// )
91+
// initGlobal*Logger(logger)
92+
// return err
93+
// }
94+
//
95+
// The initLogger() helper:
96+
// 1. Attempts to create the log directory (if needed)
97+
// 2. Opens the log file with specified flags (os.O_APPEND, os.O_TRUNC, etc.)
98+
// 3. Calls setupFunc to configure the logger instance
99+
// 4. On error, calls errorHandler to implement fallback behavior
100+
// 5. Returns the initialized logger and any error
101+
//
102+
// Fallback Behavior Strategies:
103+
//
104+
// Different logger types implement different fallback strategies based on their purpose:
105+
//
106+
// 1. FileLogger - Stdout Fallback:
107+
// - Purpose: Operational logs must always be visible
108+
// - Fallback: Redirects to stdout if log directory/file creation fails
109+
// - Error: Returns nil (never fails, always provides output)
110+
// - Use case: Critical operational messages that must be seen
111+
//
112+
// Example error handler:
113+
// func(err error, logDir, fileName string) (*FileLogger, error) {
114+
// log.Printf("WARNING: Failed to initialize log file: %v", err)
115+
// log.Printf("WARNING: Falling back to stdout for logging")
116+
// return &FileLogger{
117+
// logDir: logDir,
118+
// fileName: fileName,
119+
// useFallback: true,
120+
// logger: log.New(os.Stdout, "", 0),
121+
// }, nil
122+
// }
123+
//
124+
// 2. MarkdownLogger - Silent Fallback:
125+
// - Purpose: GitHub workflow preview logs (optional enhancement)
126+
// - Fallback: Sets useFallback flag, produces no output
127+
// - Error: Returns nil (never fails, silently disables)
128+
// - Use case: Nice-to-have logs that shouldn't block operations
129+
//
130+
// Example error handler:
131+
// func(err error, logDir, fileName string) (*MarkdownLogger, error) {
132+
// return &MarkdownLogger{
133+
// logDir: logDir,
134+
// fileName: fileName,
135+
// useFallback: true,
136+
// }, nil
137+
// }
138+
//
139+
// 3. JSONLLogger - Strict Mode:
140+
// - Purpose: Machine-readable RPC message logs for analysis
141+
// - Fallback: None - returns error immediately
142+
// - Error: Returns error to caller
143+
// - Use case: Structured data that requires file storage
144+
//
145+
// Example error handler:
146+
// func(err error, logDir, fileName string) (*JSONLLogger, error) {
147+
// return nil, err
148+
// }
149+
//
150+
// 4. ServerFileLogger - Unified Fallback:
151+
// - Purpose: Per-server log files for troubleshooting
152+
// - Fallback: Sets useFallback flag, logs to unified logger only
153+
// - Error: Returns nil (never fails, falls back to unified logging)
154+
// - Use case: Per-server logs are helpful but not required
155+
//
156+
// Note: ServerFileLogger doesn't use initLogger() because it creates
157+
// files on-demand, but follows the same fallback philosophy.
158+
//
159+
// Global Logger Management:
160+
//
161+
// After initialization, all logger types register themselves as global loggers
162+
// using the generic initGlobal*Logger() helpers from global_helpers.go:
163+
//
164+
// - initGlobalFileLogger()
165+
// - initGlobalJSONLLogger()
166+
// - initGlobalMarkdownLogger()
167+
// - initGlobalServerFileLogger()
168+
//
169+
// These helpers ensure thread-safe initialization with proper cleanup of any
170+
// existing logger instance.
171+
//
172+
// When to Use Each Logger Type:
173+
//
174+
// - FileLogger: Required operational logs (startup, errors, warnings)
175+
// - MarkdownLogger: Optional GitHub workflow preview logs
176+
// - JSONLLogger: Structured RPC message logs for tooling/analysis
177+
// - ServerFileLogger: Per-server troubleshooting logs
178+
//
179+
// Adding a New Logger Type:
180+
//
181+
// When adding a new logger type:
182+
// 1. Implement Close() method following the Close Pattern (above)
183+
// 2. Add type to closableLogger constraint in global_helpers.go
184+
// 3. Use initLogger() for initialization with appropriate fallback strategy
185+
// 4. Add initGlobal*Logger() and closeGlobal*Logger() helpers
186+
// 5. Document the fallback strategy and use case
187+
//
188+
// This consistent pattern ensures:
189+
// - Predictable behavior across all loggers
190+
// - Easy to understand fallback strategies
191+
// - Minimal code duplication
192+
// - Type-safe global logger management
193+
//
194+
// See file_logger.go, jsonl_logger.go, markdown_logger.go, and server_file_logger.go
195+
// for complete implementation examples.
196+
75197
// closeLogFile is a common helper for closing log files with consistent error handling.
76198
// It syncs buffered data before closing and handles errors appropriately.
77199
// The mutex should already be held by the caller.
@@ -148,7 +270,7 @@ type loggerErrorHandlerFunc[T closableLogger] func(err error, logDir, fileName s
148270
// - fileName: Name of the log file
149271
// - flags: File opening flags (e.g., os.O_APPEND, os.O_TRUNC)
150272
// - setup: Function to configure the logger after the file is opened
151-
// - onError: Function to handle initialization errors (can return fallback or error)
273+
// - onError: Function to handle initialization errors (implements fallback strategy)
152274
//
153275
// Returns:
154276
// - T: The initialized logger instance
@@ -157,7 +279,13 @@ type loggerErrorHandlerFunc[T closableLogger] func(err error, logDir, fileName s
157279
// This function:
158280
// 1. Attempts to open the log file with the specified flags
159281
// 2. If successful, calls the setup function to configure the logger
160-
// 3. If unsuccessful, calls the error handler to decide on fallback behavior
282+
// 3. If unsuccessful, calls the error handler to implement the logger's fallback strategy
283+
//
284+
// The onError handler determines the fallback behavior. See "Initialization Pattern for Logger Types"
285+
// documentation above for details on fallback strategies:
286+
// - FileLogger: Falls back to stdout
287+
// - MarkdownLogger: Silent fallback (no output)
288+
// - JSONLLogger: Returns error (no fallback)
161289
func initLogger[T closableLogger](
162290
logDir, fileName string,
163291
flags int,

internal/logger/global_helpers.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ package logger
1717
import "sync"
1818

1919
// closableLogger is a constraint for types that have a Close method.
20-
// This is satisfied by *FileLogger, *JSONLLogger, and *MarkdownLogger.
20+
// This is satisfied by *FileLogger, *JSONLLogger, *MarkdownLogger, and *ServerFileLogger.
2121
type closableLogger interface {
22-
*FileLogger | *JSONLLogger | *MarkdownLogger
22+
*FileLogger | *JSONLLogger | *MarkdownLogger | *ServerFileLogger
2323
Close() error
2424
}
2525

@@ -113,3 +113,13 @@ func initGlobalMarkdownLogger(logger *MarkdownLogger) {
113113
func closeGlobalMarkdownLogger() error {
114114
return closeGlobalLogger(&globalMarkdownMu, &globalMarkdownLogger)
115115
}
116+
117+
// initGlobalServerFileLogger initializes the global ServerFileLogger using the generic helper.
118+
func initGlobalServerFileLogger(logger *ServerFileLogger) {
119+
initGlobalLogger(&globalServerLoggerMu, &globalServerFileLogger, logger)
120+
}
121+
122+
// closeGlobalServerFileLogger closes the global ServerFileLogger using the generic helper.
123+
func closeGlobalServerFileLogger() error {
124+
return closeGlobalLogger(&globalServerLoggerMu, &globalServerFileLogger)
125+
}

internal/logger/server_file_logger.go

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -212,24 +212,5 @@ func LogDebugWithServer(serverID, category, format string, args ...interface{})
212212

213213
// CloseServerFileLogger closes the global server file logger
214214
func CloseServerFileLogger() error {
215-
globalServerLoggerMu.Lock()
216-
defer globalServerLoggerMu.Unlock()
217-
218-
if globalServerFileLogger != nil {
219-
err := globalServerFileLogger.Close()
220-
globalServerFileLogger = nil
221-
return err
222-
}
223-
return nil
224-
}
225-
226-
// Helper function for initializing the global server file logger
227-
func initGlobalServerFileLogger(logger *ServerFileLogger) {
228-
globalServerLoggerMu.Lock()
229-
defer globalServerLoggerMu.Unlock()
230-
231-
if globalServerFileLogger != nil {
232-
globalServerFileLogger.Close()
233-
}
234-
globalServerFileLogger = logger
215+
return closeGlobalServerFileLogger()
235216
}

0 commit comments

Comments
 (0)