Skip to content

Commit 8aabce7

Browse files
authored
Merge pull request #149 from githubnext/copilot/add-second-log-file-writer
2 parents b64734a + d989b0e commit 8aabce7

6 files changed

Lines changed: 589 additions & 12 deletions

File tree

internal/cmd/root.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,14 @@ func run(cmd *cobra.Command, args []string) error {
9797
}
9898
defer logger.CloseGlobalLogger()
9999

100-
logger.LogInfo("startup", "MCPG Gateway version: %s", version)
101-
logger.LogInfo("startup", "Starting MCPG with config: %s, listen: %s, log-dir: %s", configFile, listenAddr, logDir)
100+
// Initialize markdown logger for GitHub workflow preview
101+
if err := logger.InitMarkdownLogger(logDir, "gateway.md"); err != nil {
102+
log.Printf("Warning: Failed to initialize markdown logger: %v", err)
103+
}
104+
defer logger.CloseMarkdownLogger()
105+
106+
logger.LogInfoMd("startup", "MCPG Gateway version: %s", version)
107+
logger.LogInfoMd("startup", "Starting MCPG with config: %s, listen: %s, log-dir: %s", configFile, listenAddr, logDir)
102108
debugLog.Printf("Starting MCPG with config: %s, listen: %s", configFile, listenAddr)
103109

104110
// Load .env file if specified
@@ -114,10 +120,10 @@ func run(cmd *cobra.Command, args []string) error {
114120
debugLog.Printf("Validating execution environment...")
115121
result := config.ValidateExecutionEnvironment()
116122
if !result.IsValid() {
117-
logger.LogError("startup", "Environment validation failed: %s", result.Error())
123+
logger.LogErrorMd("startup", "Environment validation failed: %s", result.Error())
118124
return fmt.Errorf("environment validation failed: %s", result.Error())
119125
}
120-
logger.LogInfo("startup", "Environment validation passed")
126+
logger.LogInfoMd("startup", "Environment validation passed")
121127
log.Println("Environment validation passed")
122128
}
123129

@@ -172,10 +178,11 @@ func run(cmd *cobra.Command, args []string) error {
172178

173179
go func() {
174180
<-sigChan
175-
logger.LogInfo("shutdown", "Shutting down gateway...")
181+
logger.LogInfoMd("shutdown", "Shutting down gateway...")
176182
log.Println("Shutting down...")
177183
cancel()
178184
unifiedServer.Close()
185+
logger.CloseMarkdownLogger()
179186
logger.CloseGlobalLogger()
180187
os.Exit(0)
181188
}()

internal/logger/markdown_logger.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package logger
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"regexp"
8+
"strings"
9+
"sync"
10+
)
11+
12+
// MarkdownLogger manages logging to a markdown file for GitHub workflow previews
13+
type MarkdownLogger struct {
14+
logFile *os.File
15+
mu sync.Mutex
16+
logDir string
17+
fileName string
18+
useFallback bool
19+
initialized bool
20+
}
21+
22+
var (
23+
globalMarkdownLogger *MarkdownLogger
24+
globalMarkdownMu sync.RWMutex
25+
// Patterns for detecting potential secrets (simple heuristics)
26+
secretPatterns = []*regexp.Regexp{
27+
regexp.MustCompile(`(?i)(token|key|secret|password|auth)[=:]\s*[^\s]{8,}`),
28+
regexp.MustCompile(`ghp_[a-zA-Z0-9]{36,}`), // GitHub PATs
29+
regexp.MustCompile(`github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}`), // GitHub fine-grained PATs
30+
regexp.MustCompile(`(?i)bearer\s+[a-zA-Z0-9\-._~+/]+=*`), // Bearer tokens
31+
regexp.MustCompile(`(?i)authorization:\s*[a-zA-Z0-9\-._~+/]+=*`), // Auth headers
32+
regexp.MustCompile(`[a-f0-9]{32,}`), // Long hex strings (API keys)
33+
regexp.MustCompile(`(?i)(apikey|api_key|access_key)[=:]\s*[^\s]{8,}`), // API keys
34+
regexp.MustCompile(`(?i)(client_secret|client_id)[=:]\s*[^\s]{8,}`), // OAuth secrets
35+
regexp.MustCompile(`[a-zA-Z0-9_-]{20,}\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+`), // JWT tokens
36+
}
37+
)
38+
39+
// InitMarkdownLogger initializes the global markdown logger
40+
func InitMarkdownLogger(logDir, fileName string) error {
41+
globalMarkdownMu.Lock()
42+
defer globalMarkdownMu.Unlock()
43+
44+
if globalMarkdownLogger != nil {
45+
// Close existing logger
46+
globalMarkdownLogger.Close()
47+
}
48+
49+
ml := &MarkdownLogger{
50+
logDir: logDir,
51+
fileName: fileName,
52+
}
53+
54+
// Try to create the log directory if it doesn't exist
55+
if err := os.MkdirAll(logDir, 0755); err != nil {
56+
ml.useFallback = true
57+
globalMarkdownLogger = ml
58+
return nil
59+
}
60+
61+
// Try to open the log file
62+
logPath := filepath.Join(logDir, fileName)
63+
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
64+
if err != nil {
65+
ml.useFallback = true
66+
globalMarkdownLogger = ml
67+
return nil
68+
}
69+
70+
ml.logFile = file
71+
ml.initialized = false // Will be initialized on first write
72+
73+
globalMarkdownLogger = ml
74+
return nil
75+
}
76+
77+
// initializeFile writes the HTML details header on first write
78+
func (ml *MarkdownLogger) initializeFile() error {
79+
if ml.initialized {
80+
return nil
81+
}
82+
83+
if ml.logFile != nil {
84+
header := "<details>\n<summary>MCP Gateway</summary>\n\n"
85+
if _, err := ml.logFile.WriteString(header); err != nil {
86+
return err
87+
}
88+
ml.initialized = true
89+
}
90+
return nil
91+
}
92+
93+
// Close closes the log file and writes the closing details tag
94+
func (ml *MarkdownLogger) Close() error {
95+
ml.mu.Lock()
96+
defer ml.mu.Unlock()
97+
98+
if ml.logFile != nil {
99+
// Write closing details tag
100+
footer := "\n</details>\n"
101+
if _, err := ml.logFile.WriteString(footer); err != nil {
102+
// Sync any remaining buffered data before closing
103+
_ = ml.logFile.Sync() // Continue with close even if sync fails
104+
return ml.logFile.Close()
105+
}
106+
107+
// Sync and close
108+
_ = ml.logFile.Sync() // Continue with close even if sync fails
109+
return ml.logFile.Close()
110+
}
111+
return nil
112+
}
113+
114+
// sanitizeSecrets replaces potential secrets with [REDACTED]
115+
func sanitizeSecrets(message string) string {
116+
result := message
117+
for _, pattern := range secretPatterns {
118+
result = pattern.ReplaceAllStringFunc(result, func(match string) string {
119+
// Keep the prefix (key name) but redact the value
120+
if strings.Contains(match, "=") || strings.Contains(match, ":") {
121+
parts := regexp.MustCompile(`[=:]\s*`).Split(match, 2)
122+
if len(parts) == 2 {
123+
return parts[0] + "=[REDACTED]"
124+
}
125+
}
126+
// For tokens without key=value format, redact entirely
127+
return "[REDACTED]"
128+
})
129+
}
130+
return result
131+
}
132+
133+
// getEmojiForLevel returns the appropriate emoji for the log level
134+
func getEmojiForLevel(level LogLevel) string {
135+
switch level {
136+
case LogLevelInfo:
137+
return "✓"
138+
case LogLevelWarn:
139+
return "⚠️"
140+
case LogLevelError:
141+
return "✗"
142+
case LogLevelDebug:
143+
return "🔍"
144+
default:
145+
return "•"
146+
}
147+
}
148+
149+
// Log writes a log message in markdown format with emoji bullet points
150+
func (ml *MarkdownLogger) Log(level LogLevel, category, format string, args ...interface{}) {
151+
ml.mu.Lock()
152+
defer ml.mu.Unlock()
153+
154+
if ml.useFallback {
155+
return
156+
}
157+
158+
// Initialize file with header on first write
159+
if err := ml.initializeFile(); err != nil {
160+
return
161+
}
162+
163+
message := fmt.Sprintf(format, args...)
164+
165+
// Sanitize potential secrets
166+
message = sanitizeSecrets(message)
167+
168+
emoji := getEmojiForLevel(level)
169+
170+
// Format as markdown bullet point with emoji
171+
// Use code blocks for multi-line content or technical details
172+
var logLine string
173+
if strings.Contains(message, "\n") || strings.Contains(message, "command=") || strings.Contains(message, "args=") {
174+
// Multi-line or technical content - use code block
175+
logLine = fmt.Sprintf("- %s **%s**\n ```\n %s\n ```\n", emoji, category, message)
176+
} else {
177+
// Simple single-line message
178+
logLine = fmt.Sprintf("- %s **%s** %s\n", emoji, category, message)
179+
}
180+
181+
if ml.logFile != nil {
182+
if _, err := ml.logFile.WriteString(logLine); err != nil {
183+
return
184+
}
185+
// Flush immediately
186+
_ = ml.logFile.Sync() // Ignore sync errors
187+
}
188+
}
189+
190+
// Global logging functions that also write to markdown logger
191+
192+
// LogInfoMd logs to both regular and markdown loggers
193+
func LogInfoMd(category, format string, args ...interface{}) {
194+
// Log to regular logger
195+
LogInfo(category, format, args...)
196+
197+
// Log to markdown logger
198+
globalMarkdownMu.RLock()
199+
defer globalMarkdownMu.RUnlock()
200+
201+
if globalMarkdownLogger != nil {
202+
globalMarkdownLogger.Log(LogLevelInfo, category, format, args...)
203+
}
204+
}
205+
206+
// LogWarnMd logs to both regular and markdown loggers
207+
func LogWarnMd(category, format string, args ...interface{}) {
208+
// Log to regular logger
209+
LogWarn(category, format, args...)
210+
211+
// Log to markdown logger
212+
globalMarkdownMu.RLock()
213+
defer globalMarkdownMu.RUnlock()
214+
215+
if globalMarkdownLogger != nil {
216+
globalMarkdownLogger.Log(LogLevelWarn, category, format, args...)
217+
}
218+
}
219+
220+
// LogErrorMd logs to both regular and markdown loggers
221+
func LogErrorMd(category, format string, args ...interface{}) {
222+
// Log to regular logger
223+
LogError(category, format, args...)
224+
225+
// Log to markdown logger
226+
globalMarkdownMu.RLock()
227+
defer globalMarkdownMu.RUnlock()
228+
229+
if globalMarkdownLogger != nil {
230+
globalMarkdownLogger.Log(LogLevelError, category, format, args...)
231+
}
232+
}
233+
234+
// LogDebugMd logs to both regular and markdown loggers
235+
func LogDebugMd(category, format string, args ...interface{}) {
236+
// Log to regular logger
237+
LogDebug(category, format, args...)
238+
239+
// Log to markdown logger
240+
globalMarkdownMu.RLock()
241+
defer globalMarkdownMu.RUnlock()
242+
243+
if globalMarkdownLogger != nil {
244+
globalMarkdownLogger.Log(LogLevelDebug, category, format, args...)
245+
}
246+
}
247+
248+
// CloseMarkdownLogger closes the global markdown logger
249+
func CloseMarkdownLogger() error {
250+
globalMarkdownMu.Lock()
251+
defer globalMarkdownMu.Unlock()
252+
253+
if globalMarkdownLogger != nil {
254+
err := globalMarkdownLogger.Close()
255+
globalMarkdownLogger = nil
256+
return err
257+
}
258+
return nil
259+
}

0 commit comments

Comments
 (0)