A simple, debug-style logging framework for Go that follows the pattern matching syntax of the debug npm package.
- Namespace-based logging: Each logger has a namespace (e.g.,
workflow:compiler,cli:audit) - Pattern matching: Enable/disable loggers using wildcards and exclusions via the
DEBUGenvironment variable - Printf interface: Standard printf-style formatting
- Time diff display: Shows time elapsed since last log call (like debug npm package)
- Automatic color coding: Each namespace gets a unique color when stderr is a TTY
- Zero overhead: Logger enabled state is computed once at construction time
- Thread-safe: Safe for concurrent use
- Per-ServerID Logs: Separate log files for each backend MCP server for easier troubleshooting
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.
- Each serverID gets its own log file:
{serverID}.login the log directory - Logs are also written to the main
mcp-gateway.logfor a unified view - Concurrent writes to different serverID logs are handled safely
- Fallback to unified logging if per-serverID logging cannot be initialized
import "github.com/github/gh-aw-mcpg/internal/logger"
// Log to both the unified log and the server-specific log
logger.LogInfoToServer("github", "backend", "Server started successfully")
logger.LogWarnToServer("slack", "backend", "Connection timeout")
logger.LogErrorToServer("github", "backend", "Failed to authenticate: %v", err)
logger.LogDebugToServer("notion", "backend", "Processing request: %v", req)When per-serverID logging is enabled, the log directory contains:
/tmp/gh-aw/mcp-logs/
├── mcp-gateway.log # Unified log with all messages
├── github.log # Only logs for the "github" server
├── slack.log # Only logs for the "slack" server
└── notion.log # Only logs for the "notion" server
Each server-specific log file contains only the messages related to that serverID, making it easier to debug issues with individual backend servers.
Per-serverID logging is automatically initialized when the gateway starts:
// In internal/cmd/root.go
logger.InitServerFileLogger(logDir)
defer logger.CloseAllLoggers()- Easier Debugging: View all logs for a specific server in isolation
- Reduced Noise: No need to filter through logs from other servers
- Better Troubleshooting: Quickly identify which server is having issues
- Concurrent Access: Safe to log to multiple servers simultaneously
- Backward Compatible: Falls back gracefully if initialization fails
package main
import "github.com/github/gh-aw-mcpg/internal/logger"
var log = logger.New("myapp:feature")
func main() {
log.Printf("Starting application with config: %s", config)
log.Print("Multiple", " ", "arguments")
}Output shows namespace, message, and time diff:
myapp:feature Starting application with config: production +0ns
myapp:feature Multiple arguments +125ms
Check if a logger is enabled before performing expensive operations:
if log.Enabled() {
// Do expensive work only if logging is enabled
result := expensiveOperation()
log.Printf("Result: %v", result)
}Like the debug npm package, each log shows the time elapsed since the last log call:
log.Printf("Starting task")
// ... do some work ...
log.Printf("Task completed") // Shows +2.5s (or +500ms, +100µs, etc.)Control which loggers are enabled using the DEBUG environment variable with patterns:
# Enable all loggers
DEBUG=*
# Enable all loggers in the 'workflow' namespace
DEBUG=workflow:*
# Enable specific loggers
DEBUG=workflow:compiler,cli:audit
# Enable all except specific loggers
DEBUG=*,-workflow:compiler
# Enable namespace but exclude specific patterns
DEBUG=workflow:*,-workflow:compiler:cache
# Multiple patterns with exclusions
DEBUG=workflow:*,cli:*,-workflow:testColors are automatically assigned to each namespace when:
- Stderr is a TTY (terminal)
DEBUG_COLORSis not set to0
Each namespace gets a consistent color based on a hash of its name. This makes it easy to visually distinguish between different loggers.
# Disable colors
DEBUG_COLORS=0 DEBUG=* gh aw compile workflow.md
# Colors are automatically disabled when piping output
DEBUG=* gh aw compile workflow.md 2>&1 | tee output.log*- Matches all loggersnamespace:*- Matches all loggers with the given prefix*:suffix- Matches all loggers with the given suffixprefix:*:suffix- Matches loggers with both prefix and suffix-pattern- Excludes loggers matching the pattern (takes precedence)pattern1,pattern2- Multiple patterns separated by commas
The enabled state is computed once at logger construction time based on the DEBUG environment variable. This means:
- Zero overhead for disabled loggers (simple boolean check)
DEBUGchanges after the process starts won't affect existing loggers
Each logger tracks the time of its last log call to display elapsed time, similar to the debug npm package. This helps identify performance bottlenecks and understand timing relationships between log messages.
Log output goes to two destinations:
- stderr - Colorized output with time diffs (controlled by
DEBUGenvironment variable) - file logger - Text-only output without colors or time diffs (always logged when enabled)
This dual output approach allows:
- Real-time debugging with colored, timestamped output during development
- Persistent, parseable log files for production troubleshooting
- All debug logs are captured to file, making it easier to diagnose issues after the fact
The logger provides a familiar printf-style interface that Go developers expect:
Printf(format, args...)- Formatted output (always adds newline)Print(args...)- Simple concatenation (always adds newline)
// In pkg/workflow/compiler.go
var log = logger.New("workflow:compiler")
// In pkg/cli/audit.go
var log = logger.New("cli:audit")
// In pkg/parser/frontmatter.go
var log = logger.New("parser:frontmatter")Enable with:
DEBUG=workflow:* go run main.go # Only workflow package
DEBUG=cli:*,parser:* go run main.go # CLI and parser packages
DEBUG=* go run main.go # Everythingvar compileLog = logger.New("compile")
var parseLog = logger.New("parse")
var validateLog = logger.New("validate")- The
DEBUGenvironment variable is read once when the package is initialized - Thread-safe using
sync.Mutexfor time tracking - Simple pattern matching without regex (prefix, suffix, and middle wildcards only)
- Exclusion patterns (prefixed with
-) take precedence over inclusion patterns - Time diff formatted like debug npm package (ns, µs, ms, s, m, h)
- Colors assigned using FNV-1a hash for consistent namespace-to-color mapping
- Color palette chosen for readability on both light and dark terminals
- Uses ANSI 256-color codes for better compatibility