Skip to content

Commit 6f83ea4

Browse files
authored
refactor: reduce duplicate code in loggers, validation, and session handling (#2790)
Three structural duplication patterns in `internal/logger`, `internal/config`, and `internal/server` accumulated ~500 lines of repetitive boilerplate with divergence risk. ## Logger initialization closures → named functions Each `Init*Logger` function used anonymous inline closures for setup and error handling. Extracted to named package-level functions per logger type: ```go // Before: anonymous closures inline in InitFileLogger logger, err := initLogger(logDir, fileName, os.O_APPEND, func(file *os.File, logDir, fileName string) (*FileLogger, error) { ... }, func(err error, logDir, fileName string) (*FileLogger, error) { ... }, ) // After: named functions logger, err := initLogger(logDir, fileName, os.O_APPEND, setupFileLogger, handleFileLoggerError) ``` Applied to `FileLogger`, `MarkdownLogger`, `JSONLLogger`, and `ToolsLogger`. ## Validation log helpers 30+ near-identical `logValidation.Printf(...)` calls in `validation.go` standardized via three helpers: ```go func logValidateServerStart(name, serverType string) func logValidateServerPassed(name string) func logValidateServerFailed(name, reason string) ``` ## `withLock` helper on logger types Replaced repeated `mu.Lock() / defer mu.Unlock()` preambles with a per-type `withLock(fn func() error) error` method on `FileLogger`, `JSONLLogger`, `MarkdownLogger`, and `ToolsLogger`. Updated `Close()` and write methods accordingly. ## `session.go` double-checked locking → `syncutil.GetOrCreate` `requireSession` previously hand-rolled read→check→write→double-check locking. Replaced with the existing `syncutil.GetOrCreate` helper (same pattern already used in `server_file_logger.go`): ```go isNew := false if _, err := syncutil.GetOrCreate(&us.sessionMu, us.sessions, sessionID, func() (*Session, error) { s := NewSession(sessionID, "") isNew = true return s, nil }); err != nil { return err } ``` Updated `common.go` documentation to reflect the new `withLock` and named-function patterns. > [!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-build758752077/b330/launcher.test /tmp/go-build758752077/b330/launcher.test -test.testlogfile=/tmp/go-build758752077/b330/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true go --local 86_64/as user.email` (dns block) > - Triggering command: `/tmp/go-build972084089/b334/launcher.test /tmp/go-build972084089/b334/launcher.test -test.testlogfile=/tmp/go-build972084089/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2374296311/b001/vet.cfg ache/go/1.25.8/x64/src/net 64/src/encoding/gob/dec_helpers.go cfg -I . -imultiarch ache/go/1.25.8/x-buildtags -uns�� 752077/b258/_pkg-errorsas /tmp/go-build758-ifaceassert ache/go/1.25.8/x-nilfunc _amd64.s telabs/wazero/in-atomic -D_FORTIFY_SOURC-bool ache/go/1.25.8/x-buildtags` (dns block) > - Triggering command: `/tmp/go-build2612040108/b334/launcher.test /tmp/go-build2612040108/b334/launcher.test -test.testlogfile=/tmp/go-build2612040108/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s /usr�� ry=1 ga/m4mMaA4EfUuX_-tests bash /opt/hostedtoolc/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet cfg 64/pkg/tool/linu/tmp/go-build532890590/b158/vet.cfg bash --no�� --noprofile 64/pkg/tool/linux_amd64/vet .13/x64/bash se cfg x_amd64/vet ache/go/1.25.8/x64/pkg/tool/linux_amd64/vet` (dns block) > - `https://api.github.com/graphql` > - Triggering command: `/usr/bin/gh gh issue close 2755 --repo github/gh-aw-mcpg --comment Resolved in PR #2790: refactored logger initialization closures to named functions, added validation log helpers, added withLock helpers to logger types, and refactored requireSession to use syncutil.GetOrCreate. by/734792accd854/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/compile 065 -lang=go1.25 065/init.pid 644 9221a53b1917004b-p runtime-runc/mobgithub.com/github/gh-aw-mcpg/internal/server /systemd-sysctl io.containerd.rugit -c=4 -certificates.crcopilot/duplicate-code-analysis-report 31d/log.json` (http block) > - Triggering command: `/usr/bin/gh gh issue close 2756 --repo github/gh-aw-mcpg --comment Resolved in PR #2790: refactored logger initialization closures to named functions, added validation log helpers, added withLock helpers to logger types, and refactored requireSession to use syncutil.GetOrCreate. -errorsas 065 -nilfunc` (http block) > - Triggering command: `/usr/bin/gh gh issue close 2757 --repo github/gh-aw-mcpg --comment Resolved in PR #2790: refactored logger initialization closures to named functions, added validation log helpers, added withLock helpers to logger types, and refactored requireSession to use syncutil.GetOrCreate. io.containerd.ru/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/link a8d469f49c8ee67b-o 084089/b330/vet./tmp/go-build1108576084/b001/server.test /usr/bin/runc.or-importcfg init�� 64/pkg/tool/linu-s y ctl ntime.v2.task/mogit -buildtags 64/pkg/tool/linucopilot/duplicate-code-analysis-report ctl` (http block) > - `https://api.github.com/repos/github/gh-aw-mcpg/issues/2754` > - Triggering command: `/usr/bin/gh gh api -X PATCH repos//issues/2754 -f state=closed &#34;CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt&#34;, &#34;REQUESTS_CA_Bcopilot/duplicate-code-analysis-report ntime.v2.task/modocker main -lang=go1.25 table.d/chrony-onoffline /usr�� d -n 10 /var/run/docker/runtime-runc/moby iginal /run/containerd/git --log-format 908/log.json iginal` (http block) > - `https://api.github.com/repos/github/gh-aw-mcpg/issues/2755` > - Triggering command: `/usr/bin/gh gh api -X PATCH repos//issues/2755 -f state=closed /bin/java io.containerd.ru/usr/libexec/docker/cli-plugins/docker-buildx -ifaceassert -nilfunc /bin/java /usr�� d -n 10 io.containerd.runtime.v2.task/moby/ec75429440bf8--log-format erd-shim-runc-v2 io.containerd.rugit 849bc50063d4bbbaadd json erd-shim-runc-v2-v` (http block) > - Triggering command: `/usr/bin/curl curl -s -X PATCH REDACTED -H Authorization: token ****** -H Content-Type: application/json -d {&#34;state&#34;:&#34;closed&#34;} aw-mcpg/test/integration/start_gateway_with_pipe.sh --ro�� 115b774795f51db055fc7f996e03d7a54f3702b53ed9bbf2 runtime-runc/moby re-commit-msg io.containerd.rubash functions for l--norc --systemd-cgroup--noprofile re-commit-msg` (http block) > - Triggering command: `/usr/bin/curl curl -v -X PATCH REDACTED -H Authorization: token ****** -H Content-Type: application/json -d {&#34;state&#34;:&#34;closed&#34;} csi/net-interfac-v 954f�� l/config/validation.go io.containerd.runtime.v2.task/moby/901063bdb288e--log-format bin/bash io.containerd.rubash a361f2191e0cbe7c--norc json docker` (http block) > - `https://api.github.com/repos/github/gh-aw-mcpg/issues/2756` > - Triggering command: `/usr/bin/gh gh api -X PATCH repos//issues/2756 -f state=closed /bin/java da27db1da389d8d6/usr/libexec/docker/cli-plugins/docker-buildx -ifaceassert df1eeab6e3b61bce271/log.json /bin/java /usr�� /usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib y bash ntime.v2.task/mogit --log-format df1eeab6e3b61bce. bash` (http block) > - `https://api.github.com/repos/github/gh-aw-mcpg/issues/2757` > - Triggering command: `/usr/bin/gh gh api -X PATCH repos//issues/2757 -f state=closed d-dispatcher/off.d/chrony-onoffline ntime.v2.task/modocker -buildtags d0d99cb50954aa1dd34960fd0baf0e9b. d-dispatcher/off.d/chrony-onoffline --ro�� d -n 10 y deql ntime.v2.task/mogit df1eeab6e3b61bceadd c9934293d0945041. deql` (http block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build758752077/b315/config.test /tmp/go-build758752077/b315/config.test -test.testlogfile=/tmp/go-build758752077/b315/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true go1.25.8 -c=4 -nolocalimports -importcfg /tmp/go-build758752077/b192/importcfg -pack /opt/hostedtoolcache/go/1.25.8/x64/src/net/http/httptest/httptest.go rtcf�� eee754.go W2BDEJWaf ache/go/1.25.8/x-ffile-prefix-map=/opt/hostedtoolcache/go/1.25.8/x64=/_/GOROOT pull.rebase` (dns block) > - Triggering command: `/tmp/go-build837462839/b315/config.test /tmp/go-build837462839/b315/config.test -test.testlogfile=/tmp/go-build837462839/b315/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.8/x64/src/runtime/c-p Jxl7/weSoYHfkM6pMRgR2Jxl7 x_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build972084089/b319/config.test /tmp/go-build972084089/b319/config.test -test.testlogfile=/tmp/go-build972084089/b319/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build1849658217/b202/vet.cfg 9lJ78QKSc /tmp/go-build758752077/b019/vet.cfg ache/go/1.25.8/x64/pkg/tool/linux_amd64/vet` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build758752077/b330/launcher.test /tmp/go-build758752077/b330/launcher.test -test.testlogfile=/tmp/go-build758752077/b330/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true go --local 86_64/as user.email` (dns block) > - Triggering command: `/tmp/go-build972084089/b334/launcher.test /tmp/go-build972084089/b334/launcher.test -test.testlogfile=/tmp/go-build972084089/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2374296311/b001/vet.cfg ache/go/1.25.8/x64/src/net 64/src/encoding/gob/dec_helpers.go cfg -I . -imultiarch ache/go/1.25.8/x-buildtags -uns�� 752077/b258/_pkg-errorsas /tmp/go-build758-ifaceassert ache/go/1.25.8/x-nilfunc _amd64.s telabs/wazero/in-atomic -D_FORTIFY_SOURC-bool ache/go/1.25.8/x-buildtags` (dns block) > - Triggering command: `/tmp/go-build2612040108/b334/launcher.test /tmp/go-build2612040108/b334/launcher.test -test.testlogfile=/tmp/go-build2612040108/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s /usr�� ry=1 ga/m4mMaA4EfUuX_-tests bash /opt/hostedtoolc/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet cfg 64/pkg/tool/linu/tmp/go-build532890590/b158/vet.cfg bash --no�� --noprofile 64/pkg/tool/linux_amd64/vet .13/x64/bash se cfg x_amd64/vet ache/go/1.25.8/x64/pkg/tool/linux_amd64/vet` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build758752077/b330/launcher.test /tmp/go-build758752077/b330/launcher.test -test.testlogfile=/tmp/go-build758752077/b330/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true go --local 86_64/as user.email` (dns block) > - Triggering command: `/tmp/go-build972084089/b334/launcher.test /tmp/go-build972084089/b334/launcher.test -test.testlogfile=/tmp/go-build972084089/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2374296311/b001/vet.cfg ache/go/1.25.8/x64/src/net 64/src/encoding/gob/dec_helpers.go cfg -I . -imultiarch ache/go/1.25.8/x-buildtags -uns�� 752077/b258/_pkg-errorsas /tmp/go-build758-ifaceassert ache/go/1.25.8/x-nilfunc _amd64.s telabs/wazero/in-atomic -D_FORTIFY_SOURC-bool ache/go/1.25.8/x-buildtags` (dns block) > - Triggering command: `/tmp/go-build2612040108/b334/launcher.test /tmp/go-build2612040108/b334/launcher.test -test.testlogfile=/tmp/go-build2612040108/b334/testlog.txt -test.paniconexit0 -test.timeout=10m0s /usr�� ry=1 ga/m4mMaA4EfUuX_-tests bash /opt/hostedtoolc/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet cfg 64/pkg/tool/linu/tmp/go-build532890590/b158/vet.cfg bash --no�� --noprofile 64/pkg/tool/linux_amd64/vet .13/x64/bash se cfg x_amd64/vet ache/go/1.25.8/x64/pkg/tool/linux_amd64/vet` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build758752077/b339/mcp.test /tmp/go-build758752077/b339/mcp.test -test.testlogfile=/tmp/go-build758752077/b339/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true go --local ndor/bin/as committer.name` (dns block) > - Triggering command: `/tmp/go-build972084089/b343/mcp.test /tmp/go-build972084089/b343/mcp.test -test.testlogfile=/tmp/go-build972084089/b343/testlog.txt -test.paniconexit0 -test.timeout=10m0s -uns�� -unreachable=false /tmp/go-build758752077/b059/vet.cfg ache/go/1.25.8/x64/pkg/tool/linux_amd64/vet -dumpbase _cgo_.c -dumpbase-ext ache/go/1.25.8/x-importcfg -I /opt/hostedtoolc-s -I bash --gdwarf-5 --64 -o /opt/hostedtoolc-extld=gcc` (dns block) > - Triggering command: `/tmp/go-build2612040108/b343/mcp.test /tmp/go-build2612040108/b343/mcp.test -test.testlogfile=/tmp/go-build2612040108/b343/testlog.txt -test.paniconexit0 -test.timeout=10m0s --no�� --noprofile pA/uZZaPODy050ia-buildtags docker-buildx /opt/hostedtoolc/opt/hostedtoolcache/go/1.25.8/x64/pkg/tool/linux_amd64/vet cfg 64/pkg/tool/linu/tmp/go-build532890590/b139/vet.cfg docker-buildx -ato�� /logger/common.go /logger/file_logger.go 64/pkg/tool/linux_amd64/compile -errorsas -ifaceassert -nilfunc 64/pkg/tool/linux_amd64/compile` (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 CODING AGENT TIPS --> --- 💬 Send tasks to Copilot coding agent from [Slack](https://gh.io/cca-slack-docs) and [Teams](https://gh.io/cca-teams-docs) to turn conversations into code. Copilot posts an update in your thread when it's finished.
2 parents c7c15f1 + e0855ac commit 6f83ea4

7 files changed

Lines changed: 228 additions & 204 deletions

File tree

internal/config/validation.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ var varExprPattern = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_]*)\}`)
2020

2121
var logValidation = logger.New("config:validation")
2222

23+
// logValidateServerStart logs the beginning of server config validation.
24+
func logValidateServerStart(name, serverType string) {
25+
logValidation.Printf("Validating server config: name=%s, type=%s", name, serverType)
26+
}
27+
28+
// logValidateServerPassed logs a successful server config validation.
29+
func logValidateServerPassed(name string) {
30+
logValidation.Printf("Server config validation passed: name=%s", name)
31+
}
32+
33+
// logValidateServerFailed logs a failed server config validation with the given reason.
34+
func logValidateServerFailed(name, reason string) {
35+
logValidation.Printf("Validation failed: %s, name=%s", reason, name)
36+
}
37+
2338
// expandVariablesCore is the shared implementation for variable expansion.
2439
// It works with byte slices and handles the core expansion logic, tracking undefined variables.
2540
// This eliminates code duplication between expandVariables and ExpandRawJSONVariables.
@@ -105,7 +120,7 @@ func validateMounts(mounts []string, jsonPath string) error {
105120

106121
// validateServerConfigWithCustomSchemas validates a server configuration with custom schema support
107122
func validateServerConfigWithCustomSchemas(name string, server *StdinServerConfig, customSchemas map[string]interface{}) error {
108-
logValidation.Printf("Validating server config: name=%s, type=%s", name, server.Type)
123+
logValidateServerStart(name, server.Type)
109124
jsonPath := fmt.Sprintf("mcpServers.%s", name)
110125

111126
// Validate type (empty defaults to stdio)
@@ -134,7 +149,7 @@ func validateStandardServerConfig(name string, server *StdinServerConfig, jsonPa
134149
// For stdio servers, container is required
135150
if server.Type == "stdio" || server.Type == "local" {
136151
if server.Container == "" {
137-
logValidation.Printf("Validation failed: stdio server missing container field, name=%s", name)
152+
logValidateServerFailed(name, "stdio server missing container field")
138153
return rules.MissingRequired("container", "stdio", jsonPath, "Add a 'container' field (e.g., \"ghcr.io/owner/image:tag\")")
139154
}
140155

@@ -150,16 +165,16 @@ func validateStandardServerConfig(name string, server *StdinServerConfig, jsonPa
150165
// For HTTP servers, url is required and mounts are not allowed
151166
if server.Type == "http" {
152167
if server.URL == "" {
153-
logValidation.Printf("Validation failed: HTTP server missing url field, name=%s", name)
168+
logValidateServerFailed(name, "HTTP server missing url field")
154169
return rules.MissingRequired("url", "HTTP", jsonPath, "Add a 'url' field (e.g., \"https://example.com/mcp\")")
155170
}
156171
if len(server.Mounts) > 0 {
157-
logValidation.Printf("Validation failed: HTTP server has mounts field, name=%s", name)
172+
logValidateServerFailed(name, "HTTP server has mounts field")
158173
return rules.UnsupportedField("mounts", "mounts are only supported for stdio (containerized) servers", jsonPath, "Remove the 'mounts' field from HTTP server configuration; mounts only apply to stdio servers")
159174
}
160175
}
161176

162-
logValidation.Printf("Server config validation passed: name=%s", name)
177+
logValidateServerPassed(name)
163178
return nil
164179
}
165180

@@ -403,7 +418,7 @@ func validateTOMLStdioContainerization(servers map[string]*ServerConfig) error {
403418

404419
// Check if command is Docker
405420
if cfg.Command != "docker" {
406-
logValidation.Printf("Validation failed: stdio server using non-Docker command, name=%s, command=%s", name, cfg.Command)
421+
logValidateServerFailed(name, fmt.Sprintf("stdio server using non-Docker command, command=%s", cfg.Command))
407422
return fmt.Errorf(
408423
"server '%s': stdio servers must use containerized execution (command must be 'docker', got '%s'). "+
409424
"This is required by MCP Gateway Specification Section 3.2.1 (Containerization Requirement). "+

internal/logger/common.go

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,24 @@ import (
1111

1212
// Close Pattern for Logger Types
1313
//
14-
// All logger types in this package should implement their Close() method using this pattern:
14+
// All logger types in this package implement their Close() method using the withLock
15+
// helper to ensure consistent mutex handling:
1516
//
1617
// func (l *Logger) Close() error {
17-
// l.mu.Lock()
18-
// defer l.mu.Unlock()
19-
//
20-
// // Optional: Perform cleanup before closing (e.g., write footer)
21-
// // if l.logFile != nil {
22-
// // if err := writeCleanup(); err != nil {
23-
// // return closeLogFile(l.logFile, &l.mu, "loggerName")
24-
// // }
25-
// // }
26-
//
27-
// return closeLogFile(l.logFile, &l.mu, "loggerName")
18+
// return l.withLock(func() error {
19+
// // Optional: Perform cleanup before closing (e.g., write footer)
20+
// return closeLogFile(l.logFile, &l.mu, "loggerName")
21+
// })
2822
// }
2923
//
24+
// The withLock helper (defined on each logger type) acquires the mutex, executes the
25+
// callback, then releases the mutex — ensuring the lock is always released via defer.
26+
//
3027
// Why this pattern?
3128
//
32-
// 1. Mutex protection: Acquire lock at method entry to ensure thread-safe cleanup
33-
// 2. Deferred unlock: Use defer to release lock even if errors occur
34-
// 3. Optional cleanup: Logger-specific cleanup (like MarkdownLogger's footer) goes before closeLogFile
29+
// 1. Consistent locking: withLock enforces acquire-on-enter / release-on-exit
30+
// 2. Deferred unlock: Implemented inside withLock using defer, so it's never forgotten
31+
// 3. Optional cleanup: Logger-specific cleanup (like MarkdownLogger's footer) goes inside the callback
3532
// 4. Shared helper: Always delegate to closeLogFile() for consistent sync and close behavior
3633
// 5. Error handling: Return errors from closeLogFile to indicate serious issues
3734
//
@@ -40,38 +37,28 @@ import (
4037
// Simple Close() with no cleanup (FileLogger, JSONLLogger):
4138
//
4239
// func (fl *FileLogger) Close() error {
43-
// fl.mu.Lock()
44-
// defer fl.mu.Unlock()
45-
// return closeLogFile(fl.logFile, &fl.mu, "file")
40+
// return fl.withLock(func() error {
41+
// return closeLogFile(fl.logFile, &fl.mu, "file")
42+
// })
4643
// }
4744
//
4845
// Close() with custom cleanup (MarkdownLogger):
4946
//
5047
// func (ml *MarkdownLogger) Close() error {
51-
// ml.mu.Lock()
52-
// defer ml.mu.Unlock()
53-
//
54-
// if ml.logFile != nil {
55-
// // Write closing details tag before closing
56-
// footer := "\n</details>\n"
57-
// if _, err := ml.logFile.WriteString(footer); err != nil {
58-
// // Even if footer write fails, try to close the file properly
48+
// return ml.withLock(func() error {
49+
// if ml.logFile != nil {
50+
// footer := "\n</details>\n"
51+
// if _, err := ml.logFile.WriteString(footer); err != nil {
52+
// return closeLogFile(ml.logFile, &ml.mu, "markdown")
53+
// }
5954
// return closeLogFile(ml.logFile, &ml.mu, "markdown")
6055
// }
61-
//
62-
// // Footer written successfully, now close
63-
// return closeLogFile(ml.logFile, &ml.mu, "markdown")
64-
// }
65-
// return nil
56+
// return nil
57+
// })
6658
// }
6759
//
68-
// This pattern is intentionally duplicated across logger types rather than abstracted:
69-
// - It's a standard Go idiom for wrapper methods
70-
// - The duplication is minimal (5-14 lines per type)
71-
// - Each logger can customize cleanup as needed
72-
// - The shared closeLogFile() helper eliminates complex logic duplication
73-
//
74-
// When adding a new logger type, follow this pattern to ensure consistent behavior.
60+
// When adding a new logger type, add a withLock helper and follow this pattern to ensure
61+
// consistent, safe Close() behavior.
7562

7663
// Initialization Pattern for Logger Types
7764
//
@@ -81,23 +68,21 @@ import (
8168
//
8269
// Standard Initialization Pattern:
8370
//
84-
// All logger types use the initLogger() generic helper function for initialization:
71+
// All logger types use the initLogger() generic helper function for initialization.
72+
// The setup and error-handler callbacks are defined as named package-level functions
73+
// (e.g., setupFileLogger, handleFileLoggerError) to aid readability and testability:
8574
//
8675
// func Init*Logger(logDir, fileName string) error {
87-
// logger, err := initLogger(
88-
// logDir, fileName, fileFlags,
89-
// setupFunc, // Configure logger after file is opened
90-
// errorHandler, // Handle initialization failures
91-
// )
76+
// logger, err := initLogger(logDir, fileName, fileFlags, setup*Logger, handle*LoggerError)
9277
// initGlobal*Logger(logger)
9378
// return err
9479
// }
9580
//
9681
// The initLogger() helper:
9782
// 1. Attempts to create the log directory (if needed)
9883
// 2. Opens the log file with specified flags (os.O_APPEND, os.O_TRUNC, etc.)
99-
// 3. Calls setupFunc to configure the logger instance
100-
// 4. On error, calls errorHandler to implement fallback behavior
84+
// 3. Calls setup*Logger to configure the logger instance
85+
// 4. On error, calls handle*LoggerError to implement fallback behavior
10186
// 5. Returns the initialized logger and any error
10287
//
10388
// Fallback Behavior Strategies:

internal/logger/file_logger.go

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,46 +23,52 @@ var (
2323
globalLoggerMu sync.RWMutex
2424
)
2525

26+
// setupFileLogger configures a FileLogger after the log file has been opened.
27+
func setupFileLogger(file *os.File, logDir, fileName string) (*FileLogger, error) {
28+
fl := &FileLogger{
29+
logDir: logDir,
30+
fileName: fileName,
31+
logFile: file,
32+
logger: log.New(file, "", 0),
33+
}
34+
log.Printf("Logging to file: %s", filepath.Join(logDir, fileName))
35+
return fl, nil
36+
}
37+
38+
// handleFileLoggerError falls back to stdout when the log file cannot be opened.
39+
func handleFileLoggerError(err error, logDir, fileName string) (*FileLogger, error) {
40+
log.Printf("WARNING: Failed to initialize log file: %v", err)
41+
log.Printf("WARNING: Falling back to stdout for logging")
42+
fl := &FileLogger{
43+
logDir: logDir,
44+
fileName: fileName,
45+
useFallback: true,
46+
logger: log.New(os.Stdout, "", 0),
47+
}
48+
return fl, nil
49+
}
50+
2651
// InitFileLogger initializes the global file logger
2752
// If the log directory doesn't exist and can't be created, falls back to stdout
2853
func InitFileLogger(logDir, fileName string) error {
29-
logger, err := initLogger(
30-
logDir, fileName, os.O_APPEND,
31-
// Setup function: configure the logger after file is opened
32-
func(file *os.File, logDir, fileName string) (*FileLogger, error) {
33-
fl := &FileLogger{
34-
logDir: logDir,
35-
fileName: fileName,
36-
logFile: file,
37-
logger: log.New(file, "", 0),
38-
}
39-
log.Printf("Logging to file: %s", filepath.Join(logDir, fileName))
40-
return fl, nil
41-
},
42-
// Error handler: fallback to stdout on error
43-
func(err error, logDir, fileName string) (*FileLogger, error) {
44-
log.Printf("WARNING: Failed to initialize log file: %v", err)
45-
log.Printf("WARNING: Falling back to stdout for logging")
46-
fl := &FileLogger{
47-
logDir: logDir,
48-
fileName: fileName,
49-
useFallback: true,
50-
logger: log.New(os.Stdout, "", 0), // We'll add our own timestamp
51-
}
52-
return fl, nil
53-
},
54-
)
55-
54+
logger, err := initLogger(logDir, fileName, os.O_APPEND, setupFileLogger, handleFileLoggerError)
5655
initGlobalFileLogger(logger)
5756
return err
5857
}
5958

60-
// Close closes the log file
61-
func (fl *FileLogger) Close() error {
59+
// withLock acquires fl.mu, executes fn, then releases fl.mu.
60+
// Use this in methods that return an error to avoid repeating the lock/unlock preamble.
61+
func (fl *FileLogger) withLock(fn func() error) error {
6262
fl.mu.Lock()
6363
defer fl.mu.Unlock()
64+
return fn()
65+
}
6466

65-
return closeLogFile(fl.logFile, &fl.mu, "file")
67+
// Close closes the log file
68+
func (fl *FileLogger) Close() error {
69+
return fl.withLock(func() error {
70+
return closeLogFile(fl.logFile, &fl.mu, "file")
71+
})
6672
}
6773

6874
// LogLevel represents the severity of a log message

internal/logger/jsonl_logger.go

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,25 @@ type JSONLRPCMessage struct {
3737
Payload json.RawMessage `json:"payload"` // Full sanitized payload as raw JSON
3838
}
3939

40+
// setupJSONLLogger configures a JSONLLogger after the log file has been opened.
41+
func setupJSONLLogger(file *os.File, logDir, fileName string) (*JSONLLogger, error) {
42+
jl := &JSONLLogger{
43+
logDir: logDir,
44+
fileName: fileName,
45+
logFile: file,
46+
encoder: json.NewEncoder(file),
47+
}
48+
return jl, nil
49+
}
50+
51+
// handleJSONLLoggerError returns the error immediately — JSONLLogger has no fallback mode.
52+
func handleJSONLLoggerError(err error, _ string, _ string) (*JSONLLogger, error) {
53+
return nil, err
54+
}
55+
4056
// InitJSONLLogger initializes the global JSONL logger
4157
func InitJSONLLogger(logDir, fileName string) error {
42-
logger, err := initLogger(
43-
logDir, fileName, os.O_APPEND,
44-
// Setup function: configure the logger after file is opened
45-
func(file *os.File, logDir, fileName string) (*JSONLLogger, error) {
46-
jl := &JSONLLogger{
47-
logDir: logDir,
48-
fileName: fileName,
49-
logFile: file,
50-
encoder: json.NewEncoder(file),
51-
}
52-
return jl, nil
53-
},
54-
// Error handler: return error immediately (no fallback)
55-
func(err error, logDir, fileName string) (*JSONLLogger, error) {
56-
return nil, err
57-
},
58-
)
58+
logger, err := initLogger(logDir, fileName, os.O_APPEND, setupJSONLLogger, handleJSONLLoggerError)
5959

6060
// Only initialize global logger if successful (no error)
6161
// Unlike FileLogger/MarkdownLogger which return fallback loggers,
@@ -67,12 +67,19 @@ func InitJSONLLogger(logDir, fileName string) error {
6767
return err
6868
}
6969

70-
// Close closes the JSONL log file
71-
func (jl *JSONLLogger) Close() error {
70+
// withLock acquires jl.mu, executes fn, then releases jl.mu.
71+
// Use this in methods that return an error to avoid repeating the lock/unlock preamble.
72+
func (jl *JSONLLogger) withLock(fn func() error) error {
7273
jl.mu.Lock()
7374
defer jl.mu.Unlock()
75+
return fn()
76+
}
7477

75-
return closeLogFile(jl.logFile, &jl.mu, "JSONL")
78+
// Close closes the JSONL log file
79+
func (jl *JSONLLogger) Close() error {
80+
return jl.withLock(func() error {
81+
return closeLogFile(jl.logFile, &jl.mu, "JSONL")
82+
})
7683
}
7784

7885
// LogMessage logs an RPC message to the JSONL file

0 commit comments

Comments
 (0)