Commit 5220bfb
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.3 files changed
Lines changed: 143 additions & 24 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
75 | 197 | | |
76 | 198 | | |
77 | 199 | | |
| |||
148 | 270 | | |
149 | 271 | | |
150 | 272 | | |
151 | | - | |
| 273 | + | |
152 | 274 | | |
153 | 275 | | |
154 | 276 | | |
| |||
157 | 279 | | |
158 | 280 | | |
159 | 281 | | |
160 | | - | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
161 | 289 | | |
162 | 290 | | |
163 | 291 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | | - | |
216 | | - | |
217 | | - | |
218 | | - | |
219 | | - | |
220 | | - | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
| 215 | + | |
235 | 216 | | |
0 commit comments