Skip to content

Commit cb52c93

Browse files
authored
Add per-serverID log files for easier backend troubleshooting (#724)
## Per-ServerID Logging Implementation ✅ This PR implements per-serverID log files to make it easier to troubleshoot individual MCP servers. ### Completed Tasks - [x] Explore the repository and understand current logging infrastructure - [x] Design per-serverID logging architecture - [x] Add new `ServerFileLogger` that manages per-serverID log files - [x] Create log files named `{serverID}.log` in the log directory - [x] Add helper functions to log with serverID context - [x] Implement per-serverID file logger - [x] Create `ServerFileLogger` struct with map of serverID -> file logger - [x] Add `LogWithServer` functions that route to per-serverID files - [x] Handle concurrent access with proper locking - [x] Update existing log calls to use per-serverID logging - [x] Update backend-related log calls with serverID context - [x] Updated launcher package to use per-serverID logging - [x] Updated connection.go stderr logging - [x] Updated unified server tool registration logging - [x] Keep gateway-wide logs in `mcp-gateway.log` - [x] Add tests for per-serverID logging - [x] Test log file creation per serverID - [x] Test concurrent logging to different serverID files - [x] Test all log levels - [x] Test fallback mode - [x] Test multiple initialization - [x] Test that unified log is preserved ✨ - [x] Documentation - [x] Update logger README with per-serverID logging details - [x] Update main README to highlight the feature - [x] Update AGENTS.md with logging details - [x] Document log directory structure - [x] Verify changes - [x] Build successful - [x] All unit tests passing - [x] Linter passed ✅ (fixed gofmt issue) - [x] Code review completed - addressed feedback - [x] Security check (CodeQL) - No vulnerabilities found ### Implementation Summary **Key Feature - Dual Logging:** ✅ Each per-serverID log function writes to BOTH: 1. Server-specific log file (e.g., `github.log`) - isolated messages 2. Unified `mcp-gateway.log` - all messages with `[serverID]` prefix This ensures **single-view files are preserved** while adding per-serverID isolation. **Log Directory Structure:** ``` /tmp/gh-aw/mcp-logs/ ├── mcp-gateway.log # ALL messages (single-view) ✅ ├── github.log # Only GitHub server logs ├── slack.log # Only Slack server logs ├── notion.log # Only Notion server logs ├── gateway.md # Markdown format └── rpc-messages.jsonl # RPC messages ``` **API Usage:** ```go // Writes to both {serverID}.log AND mcp-gateway.log logger.LogInfoWithServer("github", "backend", "Server started") logger.LogWarnWithServer("slack", "backend", "Connection timeout") ``` **Testing:** - 8 comprehensive test cases including test for unified log preservation - All existing tests continue to pass - Verified single-view and per-serverID logs work correctly - CodeQL security check passed with no vulnerabilities - All linting checks pass ✅ <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/github/gh-aw-mcpg/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.
2 parents 745db3d + 6602744 commit cb52c93

10 files changed

Lines changed: 630 additions & 23 deletions

File tree

AGENTS.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,23 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
366366
- `MCP_GATEWAY_PAYLOAD_DIR` - Large payload storage directory (sets default for `--payload-dir` flag, default: `/tmp/jq-payloads`)
367367

368368
**File Logging:**
369-
- Operational logs are always written to `mcp-gateway.log` in the configured log directory
369+
- Operational logs are always written to log files in the configured log directory
370370
- Default log directory: `/tmp/gh-aw/mcp-logs` (configurable via `--log-dir` flag or `MCP_GATEWAY_LOG_DIR` env var)
371371
- Falls back to stdout if log directory cannot be created
372+
- **Log Files Created:**
373+
- `mcp-gateway.log` - Unified log with all messages
374+
- `{serverID}.log` - Per-server logs (e.g., `github.log`, `slack.log`) for easier troubleshooting
375+
- `gateway.md` - Markdown-formatted logs for GitHub workflow previews
376+
- `rpc-messages.jsonl` - Machine-readable JSONL format for RPC message analysis
372377
- Logs include: startup, client interactions, backend operations, auth events, errors
373378

379+
**Per-ServerID Logging:**
380+
- Each backend MCP server gets its own log file for easier troubleshooting
381+
- Use `LogInfoWithServer`, `LogWarnWithServer`, `LogErrorWithServer`, `LogDebugWithServer` functions
382+
- Example: `logger.LogInfoWithServer("github", "backend", "Server started successfully")`
383+
- Logs are written to both the server-specific file and the unified `mcp-gateway.log`
384+
- Thread-safe concurrent logging with automatic fallback
385+
374386
**Large Payload Handling:**
375387
- Large tool response payloads are stored in the configured payload directory
376388
- Default payload directory: `/tmp/jq-payloads` (configurable via `--payload-dir` flag, `MCP_GATEWAY_PAYLOAD_DIR` env var, or `payload_dir` in config)

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This gateway is used with [GitHub Agentic Workflows](https://github.com/github/g
2323
- **Stdio Transport**: JSON-RPC 2.0 over stdin/stdout for MCP communication
2424
- **Container Detection**: Automatic detection of containerized environments with security warnings
2525
- **Enhanced Debugging**: Detailed error context and troubleshooting suggestions for command failures
26+
- **Per-ServerID Logs**: Separate log files for each backend MCP server (`{serverID}.log`) for easier troubleshooting
2627

2728
## Getting Started
2829

@@ -67,6 +68,10 @@ For detailed setup instructions, building from source, and local development, se
6768
- `-e MCP_GATEWAY_*`: Required environment variables
6869
- `-v /var/run/docker.sock`: Required for spawning backend MCP servers
6970
- `-v /path/to/logs:/tmp/gh-aw/mcp-logs`: Mount for persistent gateway logs (or use `-e MCP_GATEWAY_LOG_DIR=/custom/path` with matching volume mount)
71+
- `mcp-gateway.log`: Unified log with all messages
72+
- `{serverID}.log`: Per-server logs for easier troubleshooting
73+
- `gateway.md`: Markdown-formatted logs for GitHub workflow previews
74+
- `rpc-messages.jsonl`: Machine-readable RPC message logs
7075
- `-p 8000:8000`: Port mapping must match `MCP_GATEWAY_PORT`
7176

7277
MCPG will start in routed mode on `http://0.0.0.0:8000` (using `MCP_GATEWAY_PORT`), proxying MCP requests to your configured backend servers.
@@ -329,15 +334,44 @@ MCP_GATEWAY_PORT=3000 ./run.sh
329334

330335
MCPG provides comprehensive logging of all gateway operations to help diagnose issues and monitor activity.
331336

337+
### Log Files
338+
339+
The gateway creates multiple log files for different purposes:
340+
341+
1. **`mcp-gateway.log`** - Unified log with all gateway messages
342+
2. **`{serverID}.log`** - Per-server logs (e.g., `github.log`, `slack.log`) for easier troubleshooting of specific backend servers
343+
3. **`gateway.md`** - Markdown-formatted logs for GitHub workflow previews
344+
4. **`rpc-messages.jsonl`** - Machine-readable JSONL format for RPC message analysis
345+
332346
### Log File Location
333347

334-
By default, logs are written to `/tmp/gh-aw/mcp-logs/mcp-gateway.log`. This location can be configured using either:
348+
By default, logs are written to `/tmp/gh-aw/mcp-logs/`. This location can be configured using either:
335349

336350
1. **`MCP_GATEWAY_LOG_DIR` environment variable** - Sets the default log directory
337351
2. **`--log-dir` flag** - Overrides the environment variable and default
338352

339353
The precedence order is: `--log-dir` flag → `MCP_GATEWAY_LOG_DIR` env var → default (`/tmp/gh-aw/mcp-logs`)
340354

355+
### Per-ServerID Logs
356+
357+
Each backend MCP server gets its own log file (e.g., `github.log`, `slack.log`) in addition to the unified `mcp-gateway.log`. This makes it much easier to:
358+
359+
- Debug issues with a specific backend server
360+
- View all activity for one server without filtering
361+
- Identify which server is causing problems
362+
- Troubleshoot server-specific configuration issues
363+
364+
Example log directory structure:
365+
```
366+
/tmp/gh-aw/mcp-logs/
367+
├── mcp-gateway.log # All messages
368+
├── github.log # Only GitHub server logs
369+
├── slack.log # Only Slack server logs
370+
├── notion.log # Only Notion server logs
371+
├── gateway.md # Markdown format
372+
└── rpc-messages.jsonl # RPC messages
373+
```
374+
341375
**Using the environment variable:**
342376
```bash
343377
export MCP_GATEWAY_LOG_DIR=/var/log/mcp-gateway

internal/cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ func run(cmd *cobra.Command, args []string) error {
138138
}
139139
defer logger.CloseGlobalLogger()
140140

141+
// Initialize per-serverID logger
142+
if err := logger.InitServerFileLogger(logDir); err != nil {
143+
log.Printf("Warning: Failed to initialize server file logger: %v", err)
144+
}
145+
defer logger.CloseServerFileLogger()
146+
141147
// Initialize markdown logger for GitHub workflow preview
142148
if err := logger.InitMarkdownLogger(logDir, "gateway.md"); err != nil {
143149
log.Printf("Warning: Failed to initialize markdown logger: %v", err)

internal/launcher/launcher.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ func New(ctx context.Context, cfg *config.Config) *Launcher {
6363

6464
// GetOrLaunch returns an existing connection or launches a new one
6565
func GetOrLaunch(l *Launcher, serverID string) (*mcp.Connection, error) {
66-
logger.LogDebug("backend", "GetOrLaunch called for server: %s", serverID)
66+
logger.LogDebugWithServer(serverID, "backend", "GetOrLaunch called for server: %s", serverID)
6767
logLauncher.Printf("GetOrLaunch called: serverID=%s", serverID)
6868

6969
// Check if already exists
7070
l.mu.RLock()
7171
if conn, ok := l.connections[serverID]; ok {
7272
l.mu.RUnlock()
73-
logger.LogDebug("backend", "Reusing existing backend connection: %s", serverID)
73+
logger.LogDebugWithServer(serverID, "backend", "Reusing existing backend connection: %s", serverID)
7474
logLauncher.Printf("Reusing existing connection: serverID=%s", serverID)
7575
return conn, nil
7676
}
@@ -84,37 +84,37 @@ func GetOrLaunch(l *Launcher, serverID string) (*mcp.Connection, error) {
8484

8585
// Double-check after acquiring write lock
8686
if conn, ok := l.connections[serverID]; ok {
87-
logger.LogDebug("backend", "Backend connection created by another goroutine: %s", serverID)
87+
logger.LogDebugWithServer(serverID, "backend", "Backend connection created by another goroutine: %s", serverID)
8888
logLauncher.Printf("Connection created by another goroutine: serverID=%s", serverID)
8989
return conn, nil
9090
}
9191

9292
// Get server config
9393
serverCfg, ok := l.config.Servers[serverID]
9494
if !ok {
95-
logger.LogError("backend", "Backend server not found in config: %s", serverID)
95+
logger.LogErrorWithServer(serverID, "backend", "Backend server not found in config: %s", serverID)
9696
logLauncher.Printf("Server not found in config: serverID=%s", serverID)
9797
return nil, fmt.Errorf("server '%s' not found in config", serverID)
9898
}
9999
logLauncher.Printf("Retrieved server config: serverID=%s, type=%s", serverID, serverCfg.Type)
100100

101101
// Handle HTTP backends differently
102102
if serverCfg.Type == "http" {
103-
logger.LogInfo("backend", "Configuring HTTP MCP backend: %s, url=%s", serverID, serverCfg.URL)
103+
logger.LogInfoWithServer(serverID, "backend", "Configuring HTTP MCP backend: %s, url=%s", serverID, serverCfg.URL)
104104
log.Printf("[LAUNCHER] Configuring HTTP MCP backend: %s", serverID)
105105
log.Printf("[LAUNCHER] URL: %s", serverCfg.URL)
106106
logLauncher.Printf("HTTP backend: serverID=%s, url=%s", serverID, serverCfg.URL)
107107

108108
// Create an HTTP connection
109109
conn, err := mcp.NewHTTPConnection(l.ctx, serverID, serverCfg.URL, serverCfg.Headers)
110110
if err != nil {
111-
logger.LogError("backend", "Failed to create HTTP connection: %s, error=%v", serverID, err)
111+
logger.LogErrorWithServer(serverID, "backend", "Failed to create HTTP connection: %s, error=%v", serverID, err)
112112
log.Printf("[LAUNCHER] ❌ FAILED to create HTTP connection for '%s'", serverID)
113113
log.Printf("[LAUNCHER] Error: %v", err)
114114
return nil, fmt.Errorf("failed to create HTTP connection: %w", err)
115115
}
116116

117-
logger.LogInfo("backend", "Successfully configured HTTP MCP backend: %s", serverID)
117+
logger.LogInfoWithServer(serverID, "backend", "Successfully configured HTTP MCP backend: %s", serverID)
118118
log.Printf("[LAUNCHER] Successfully configured HTTP backend: %s", serverID)
119119
logLauncher.Printf("HTTP connection configured: serverID=%s", serverID)
120120

@@ -178,7 +178,7 @@ func GetOrLaunch(l *Launcher, serverID string) (*mcp.Connection, error) {
178178
// GetOrLaunchForSession returns a session-aware connection or launches a new one
179179
// This is used for stateful stdio backends that require persistent connections
180180
func GetOrLaunchForSession(l *Launcher, serverID, sessionID string) (*mcp.Connection, error) {
181-
logger.LogDebug("backend", "GetOrLaunchForSession called: server=%s, session=%s", serverID, sessionID)
181+
logger.LogDebugWithServer(serverID, "backend", "GetOrLaunchForSession called: server=%s, session=%s", serverID, sessionID)
182182
logLauncher.Printf("GetOrLaunchForSession called: serverID=%s, sessionID=%s", serverID, sessionID)
183183

184184
// Get server config first to determine backend type
@@ -187,7 +187,7 @@ func GetOrLaunchForSession(l *Launcher, serverID, sessionID string) (*mcp.Connec
187187
l.mu.RUnlock()
188188

189189
if !ok {
190-
logger.LogError("backend", "Backend server not found in config: %s", serverID)
190+
logger.LogErrorWithServer(serverID, "backend", "Backend server not found in config: %s", serverID)
191191
return nil, fmt.Errorf("server '%s' not found in config", serverID)
192192
}
193193

@@ -200,7 +200,7 @@ func GetOrLaunchForSession(l *Launcher, serverID, sessionID string) (*mcp.Connec
200200
logLauncher.Printf("Checking session pool: serverID=%s, sessionID=%s", serverID, sessionID)
201201
// For stdio backends, check the session pool first
202202
if conn, exists := l.sessionPool.Get(serverID, sessionID); exists {
203-
logger.LogDebug("backend", "Reusing session connection: server=%s, session=%s", serverID, sessionID)
203+
logger.LogDebugWithServer(serverID, "backend", "Reusing session connection: server=%s, session=%s", serverID, sessionID)
204204
logLauncher.Printf("Reusing session connection: serverID=%s, sessionID=%s", serverID, sessionID)
205205
return conn, nil
206206
}
@@ -214,7 +214,7 @@ func GetOrLaunchForSession(l *Launcher, serverID, sessionID string) (*mcp.Connec
214214

215215
// Double-check after acquiring lock
216216
if conn, exists := l.sessionPool.Get(serverID, sessionID); exists {
217-
logger.LogDebug("backend", "Session connection created by another goroutine: server=%s, session=%s", serverID, sessionID)
217+
logger.LogDebugWithServer(serverID, "backend", "Session connection created by another goroutine: server=%s, session=%s", serverID, sessionID)
218218
logLauncher.Printf("Session connection created by another goroutine: serverID=%s, sessionID=%s", serverID, sessionID)
219219
return conn, nil
220220
}

internal/launcher/log_helpers.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func sessionSuffix(sessionID string) string {
2121

2222
// logSecurityWarning logs container security warnings
2323
func (l *Launcher) logSecurityWarning(serverID string, serverCfg *config.ServerConfig) {
24-
logger.LogWarn("backend", "Server '%s' uses direct command execution inside a container (command: %s)", serverID, serverCfg.Command)
24+
logger.LogWarnWithServer(serverID, "backend", "Server '%s' uses direct command execution inside a container (command: %s)", serverID, serverCfg.Command)
2525
log.Printf("[LAUNCHER] ⚠️ WARNING: Server '%s' uses direct command execution inside a container", serverID)
2626
log.Printf("[LAUNCHER] ⚠️ Security Notice: Command '%s' will execute with the same privileges as the gateway", serverCfg.Command)
2727
log.Printf("[LAUNCHER] ⚠️ Consider using 'container' field instead for better isolation")
@@ -30,11 +30,11 @@ func (l *Launcher) logSecurityWarning(serverID string, serverCfg *config.ServerC
3030
// logLaunchStart logs server launch initiation
3131
func (l *Launcher) logLaunchStart(serverID, sessionID string, serverCfg *config.ServerConfig, isDirectCommand bool) {
3232
if sessionID != "" {
33-
logger.LogInfo("backend", "Launching MCP backend server for session: server=%s, session=%s, command=%s, args=%v", serverID, sessionID, serverCfg.Command, sanitize.SanitizeArgs(serverCfg.Args))
33+
logger.LogInfoWithServer(serverID, "backend", "Launching MCP backend server for session: server=%s, session=%s, command=%s, args=%v", serverID, sessionID, serverCfg.Command, sanitize.SanitizeArgs(serverCfg.Args))
3434
log.Printf("[LAUNCHER] Starting MCP server for session: %s (session: %s)", serverID, sessionID)
3535
logLauncher.Printf("Launching new session server: serverID=%s, sessionID=%s, command=%s", serverID, sessionID, serverCfg.Command)
3636
} else {
37-
logger.LogInfo("backend", "Launching MCP backend server: %s, command=%s, args=%v", serverID, serverCfg.Command, sanitize.SanitizeArgs(serverCfg.Args))
37+
logger.LogInfoWithServer(serverID, "backend", "Launching MCP backend server: %s, command=%s, args=%v", serverID, serverCfg.Command, sanitize.SanitizeArgs(serverCfg.Args))
3838
log.Printf("[LAUNCHER] Starting MCP server: %s", serverID)
3939
logLauncher.Printf("Launching new server: serverID=%s, command=%s, inContainer=%v, isDirectCommand=%v",
4040
serverID, serverCfg.Command, l.runningInContainer, isDirectCommand)
@@ -70,7 +70,7 @@ func (l *Launcher) logEnvPassthrough(args []string) {
7070

7171
// logLaunchError logs detailed launch failure diagnostics
7272
func (l *Launcher) logLaunchError(serverID, sessionID string, err error, serverCfg *config.ServerConfig, isDirectCommand bool) {
73-
logger.LogError("backend", "Failed to launch MCP backend server%s: server=%s%s, error=%v",
73+
logger.LogErrorWithServer(serverID, "backend", "Failed to launch MCP backend server%s: server=%s%s, error=%v",
7474
sessionSuffix(sessionID), serverID, sessionSuffix(sessionID), err)
7575
log.Printf("[LAUNCHER] ❌ FAILED to launch server '%s'%s", serverID, sessionSuffix(sessionID))
7676
log.Printf("[LAUNCHER] Error: %v", err)
@@ -97,7 +97,7 @@ func (l *Launcher) logLaunchError(serverID, sessionID string, err error, serverC
9797

9898
// logTimeoutError logs startup timeout diagnostics
9999
func (l *Launcher) logTimeoutError(serverID, sessionID string) {
100-
logger.LogError("backend", "MCP backend server startup timeout%s: server=%s%s, timeout=%v",
100+
logger.LogErrorWithServer(serverID, "backend", "MCP backend server startup timeout%s: server=%s%s, timeout=%v",
101101
sessionSuffix(sessionID), serverID, sessionSuffix(sessionID), l.startupTimeout)
102102
log.Printf("[LAUNCHER] ❌ Server startup timed out after %v", l.startupTimeout)
103103
log.Printf("[LAUNCHER] ⚠️ The server may be hanging or taking too long to initialize")
@@ -112,11 +112,11 @@ func (l *Launcher) logTimeoutError(serverID, sessionID string) {
112112
// logLaunchSuccess logs successful server launch
113113
func (l *Launcher) logLaunchSuccess(serverID, sessionID string) {
114114
if sessionID != "" {
115-
logger.LogInfo("backend", "Successfully launched MCP backend server for session: server=%s, session=%s", serverID, sessionID)
115+
logger.LogInfoWithServer(serverID, "backend", "Successfully launched MCP backend server for session: server=%s, session=%s", serverID, sessionID)
116116
log.Printf("[LAUNCHER] Successfully launched: %s (session: %s)", serverID, sessionID)
117117
logLauncher.Printf("Session connection established: serverID=%s, sessionID=%s", serverID, sessionID)
118118
} else {
119-
logger.LogInfo("backend", "Successfully launched MCP backend server: %s", serverID)
119+
logger.LogInfoWithServer(serverID, "backend", "Successfully launched MCP backend server: %s", serverID)
120120
log.Printf("[LAUNCHER] Successfully launched: %s", serverID)
121121
logLauncher.Printf("Connection established: serverID=%s", serverID)
122122
}

internal/logger/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,61 @@ A simple, debug-style logging framework for Go that follows the pattern matching
1111
- **Automatic color coding**: Each namespace gets a unique color when stderr is a TTY
1212
- **Zero overhead**: Logger enabled state is computed once at construction time
1313
- **Thread-safe**: Safe for concurrent use
14+
- **Per-ServerID Logs**: Separate log files for each backend MCP server for easier troubleshooting
15+
16+
## Per-ServerID Logging
17+
18+
The logger package supports creating separate log files for each backend MCP server (identified by serverID). This makes it much easier to troubleshoot specific servers without sifting through unified logs.
19+
20+
### How It Works
21+
22+
- Each serverID gets its own log file: `{serverID}.log` in the log directory
23+
- Logs are also written to the main `mcp-gateway.log` for a unified view
24+
- Concurrent writes to different serverID logs are handled safely
25+
- Fallback to unified logging if per-serverID logging cannot be initialized
26+
27+
### Usage
28+
29+
```go
30+
import "github.com/github/gh-aw-mcpg/internal/logger"
31+
32+
// Log to both the unified log and the server-specific log
33+
logger.LogInfoWithServer("github", "backend", "Server started successfully")
34+
logger.LogWarnWithServer("slack", "backend", "Connection timeout")
35+
logger.LogErrorWithServer("github", "backend", "Failed to authenticate: %v", err)
36+
logger.LogDebugWithServer("notion", "backend", "Processing request: %v", req)
37+
```
38+
39+
### Log File Structure
40+
41+
When per-serverID logging is enabled, the log directory contains:
42+
```
43+
/tmp/gh-aw/mcp-logs/
44+
├── mcp-gateway.log # Unified log with all messages
45+
├── github.log # Only logs for the "github" server
46+
├── slack.log # Only logs for the "slack" server
47+
└── notion.log # Only logs for the "notion" server
48+
```
49+
50+
Each server-specific log file contains only the messages related to that serverID, making it easier to debug issues with individual backend servers.
51+
52+
### Initialization
53+
54+
Per-serverID logging is automatically initialized when the gateway starts:
55+
56+
```go
57+
// In internal/cmd/root.go
58+
logger.InitServerFileLogger(logDir)
59+
defer logger.CloseServerFileLogger()
60+
```
61+
62+
### Benefits
63+
64+
- **Easier Debugging**: View all logs for a specific server in isolation
65+
- **Reduced Noise**: No need to filter through logs from other servers
66+
- **Better Troubleshooting**: Quickly identify which server is having issues
67+
- **Concurrent Access**: Safe to log to multiple servers simultaneously
68+
- **Backward Compatible**: Falls back gracefully if initialization fails
1469

1570
## Usage
1671

0 commit comments

Comments
 (0)