Skip to content

Commit b457931

Browse files
authored
Merge pull request #160 from githubnext/copilot/merge-debug-into-file-logger
2 parents 3778f0b + 471e55e commit b457931

5 files changed

Lines changed: 147 additions & 3 deletions

File tree

.github/workflows/go-logger.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ Replace `pkg:filename` with the actual package and filename:
9191
- For `internal/cmd/root.go``"cmd:root"`
9292
- For `internal/launcher/docker.go``"launcher:docker"`
9393

94+
**Note:** Debug loggers created with `logger.New()` write to both:
95+
- **stderr** - Colorized output with time diffs (when `DEBUG` env var matches namespace)
96+
- **file logger** - Text-only output (always logged when the logger is enabled)
97+
98+
This dual output approach ensures all debug logs are captured to file for troubleshooting while providing real-time colored output during development.
99+
94100
### Logger Usage Patterns
95101

96102
**Good logging examples:**

AGENTS.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ import "github.com/githubnext/gh-aw-mcpg/internal/logger"
148148
// Create a logger with namespace following pkg:filename convention
149149
var log = logger.New("pkg:filename")
150150

151-
// Log debug messages (only shown when DEBUG environment variable matches)
151+
// Log debug messages
152+
// - Writes to stderr with colors and time diffs (when DEBUG matches namespace)
153+
// - Also writes to file logger as text-only (always, when logger is enabled)
152154
log.Printf("Processing %d items", count)
153155
log.Print("Simple debug message")
154156

@@ -158,7 +160,7 @@ if log.Enabled() {
158160
}
159161
```
160162

161-
**For operational/file logging, use the file logger:**
163+
**For operational/file logging, use the file logger directly:**
162164

163165
```go
164166
import "github.com/githubnext/gh-aw-mcpg/internal/logger"
@@ -170,6 +172,8 @@ logger.LogError("category", "Operation failed: %v", err)
170172
logger.LogDebug("category", "Debug details: %+v", details)
171173
```
172174

175+
**Note:** Debug loggers created with `logger.New()` now write to both stderr (with colors/time diffs) and the file logger (text-only). This provides real-time colored output during development while ensuring all debug logs are captured to file for production troubleshooting.
176+
173177
**Logging Categories:**
174178
- `startup` - Gateway initialization and configuration
175179
- `shutdown` - Graceful shutdown events

internal/logger/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,14 @@ Each logger tracks the time of its last log call to display elapsed time, simila
125125

126126
### Output Destination
127127

128-
All log output goes to **stderr** to avoid interfering with stdout data (JSON, command output, etc.).
128+
Log output goes to **two destinations**:
129+
- **stderr** - Colorized output with time diffs (controlled by `DEBUG` environment variable)
130+
- **file logger** - Text-only output without colors or time diffs (always logged when enabled)
131+
132+
This dual output approach allows:
133+
- Real-time debugging with colored, timestamped output during development
134+
- Persistent, parseable log files for production troubleshooting
135+
- All debug logs are captured to file, making it easier to diagnose issues after the fact
129136

130137
### Printf Interface
131138

internal/logger/logger.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func (l *Logger) Enabled() bool {
9999
// Printf prints a formatted message if the logger is enabled.
100100
// A newline is always added at the end.
101101
// Time diff since last log is displayed like the debug npm package.
102+
// Also writes to the file logger in text-only format.
102103
func (l *Logger) Printf(format string, args ...any) {
103104
if !l.enabled {
104105
return
@@ -110,16 +111,22 @@ func (l *Logger) Printf(format string, args ...any) {
110111
l.mu.Unlock()
111112

112113
message := fmt.Sprintf(format, args...)
114+
115+
// Write to stderr with colors and time diff
113116
if l.color != "" {
114117
fmt.Fprintf(os.Stderr, "%s%s%s %s +%s\n", l.color, l.namespace, colorReset, message, timeutil.FormatDuration(diff))
115118
} else {
116119
fmt.Fprintf(os.Stderr, "%s %s +%s\n", l.namespace, message, timeutil.FormatDuration(diff))
117120
}
121+
122+
// Also write to file logger in text-only format (no colors, no time diff)
123+
LogDebug(l.namespace, "%s", message)
118124
}
119125

120126
// Print prints a message if the logger is enabled.
121127
// A newline is always added at the end.
122128
// Time diff since last log is displayed like the debug npm package.
129+
// Also writes to the file logger in text-only format.
123130
func (l *Logger) Print(args ...any) {
124131
if !l.enabled {
125132
return
@@ -131,11 +138,16 @@ func (l *Logger) Print(args ...any) {
131138
l.mu.Unlock()
132139

133140
message := fmt.Sprint(args...)
141+
142+
// Write to stderr with colors and time diff
134143
if l.color != "" {
135144
fmt.Fprintf(os.Stderr, "%s%s%s %s +%s\n", l.color, l.namespace, colorReset, message, timeutil.FormatDuration(diff))
136145
} else {
137146
fmt.Fprintf(os.Stderr, "%s %s +%s\n", l.namespace, message, timeutil.FormatDuration(diff))
138147
}
148+
149+
// Also write to file logger in text-only format (no colors, no time diff)
150+
LogDebug(l.namespace, "%s", message)
139151
}
140152

141153
// computeEnabled computes whether a namespace matches the DEBUG patterns

internal/logger/logger_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package logger
33
import (
44
"bytes"
55
"os"
6+
"path/filepath"
67
"strings"
78
"testing"
89
"time"
@@ -391,3 +392,117 @@ func TestComputeEnabled(t *testing.T) {
391392
})
392393
}
393394
}
395+
396+
func TestDebugLoggerWritesToFile(t *testing.T) {
397+
// Create a temporary directory for the file logger
398+
tmpDir := t.TempDir()
399+
logDir := filepath.Join(tmpDir, "logs")
400+
fileName := "debug-test.log"
401+
402+
// Initialize the file logger
403+
err := InitFileLogger(logDir, fileName)
404+
if err != nil {
405+
t.Fatalf("InitFileLogger failed: %v", err)
406+
}
407+
defer CloseGlobalLogger()
408+
409+
// Enable all debug loggers
410+
debugEnv = "*"
411+
412+
// Create a debug logger
413+
log := New("test:debug")
414+
415+
// Capture stderr to verify stderr output
416+
stderrOutput := captureStderr(func() {
417+
log.Printf("Test message %d", 42)
418+
log.Print("Another test message")
419+
})
420+
421+
// Verify stderr output contains the messages
422+
if !strings.Contains(stderrOutput, "Test message 42") {
423+
t.Errorf("Stderr should contain debug message, got: %s", stderrOutput)
424+
}
425+
if !strings.Contains(stderrOutput, "Another test message") {
426+
t.Errorf("Stderr should contain debug message, got: %s", stderrOutput)
427+
}
428+
429+
// Close the file logger to flush all data
430+
CloseGlobalLogger()
431+
432+
// Read the log file
433+
logPath := filepath.Join(logDir, fileName)
434+
content, err := os.ReadFile(logPath)
435+
if err != nil {
436+
t.Fatalf("Failed to read log file: %v", err)
437+
}
438+
439+
logContent := string(content)
440+
441+
// Verify the file logger contains the same messages (text-only, no colors)
442+
if !strings.Contains(logContent, "Test message 42") {
443+
t.Errorf("Log file should contain debug message, got: %s", logContent)
444+
}
445+
if !strings.Contains(logContent, "Another test message") {
446+
t.Errorf("Log file should contain debug message, got: %s", logContent)
447+
}
448+
449+
// Verify the file logger has DEBUG level
450+
if !strings.Contains(logContent, "[DEBUG]") {
451+
t.Errorf("Log file should contain [DEBUG] level, got: %s", logContent)
452+
}
453+
454+
// Verify the file logger has the namespace as category
455+
if !strings.Contains(logContent, "[test:debug]") {
456+
t.Errorf("Log file should contain [test:debug] category, got: %s", logContent)
457+
}
458+
459+
// Verify no color codes in file output
460+
if strings.Contains(logContent, "\033[") {
461+
t.Errorf("Log file should not contain ANSI color codes, got: %s", logContent)
462+
}
463+
}
464+
465+
func TestDebugLoggerDisabledNoFileWrite(t *testing.T) {
466+
// Create a temporary directory for the file logger
467+
tmpDir := t.TempDir()
468+
logDir := filepath.Join(tmpDir, "logs")
469+
fileName := "debug-disabled-test.log"
470+
471+
// Initialize the file logger
472+
err := InitFileLogger(logDir, fileName)
473+
if err != nil {
474+
t.Fatalf("InitFileLogger failed: %v", err)
475+
}
476+
defer CloseGlobalLogger()
477+
478+
// Disable all debug loggers
479+
debugEnv = ""
480+
481+
// Create a debug logger (should be disabled)
482+
log := New("test:disabled")
483+
484+
// Verify logger is disabled
485+
if log.Enabled() {
486+
t.Fatal("Logger should be disabled when DEBUG is empty")
487+
}
488+
489+
// Try to log (should not write anywhere)
490+
log.Printf("This should not appear")
491+
492+
// Close the file logger to flush all data
493+
CloseGlobalLogger()
494+
495+
// Read the log file
496+
logPath := filepath.Join(logDir, fileName)
497+
content, err := os.ReadFile(logPath)
498+
if err != nil {
499+
t.Fatalf("Failed to read log file: %v", err)
500+
}
501+
502+
logContent := string(content)
503+
504+
// Verify the message is NOT in the file (logger was disabled)
505+
if strings.Contains(logContent, "This should not appear") {
506+
t.Errorf("Disabled logger should not write to file, got: %s", logContent)
507+
}
508+
}

0 commit comments

Comments
 (0)