|
| 1 | +// Package logger provides structured logging for the MCP Gateway. |
| 2 | +// |
| 3 | +// This file implements logging of MCP server tools to a JSON file (tools.json). |
| 4 | +// It maintains a mapping of server IDs to their available tools with names and descriptions. |
| 5 | +package logger |
| 6 | + |
| 7 | +import ( |
| 8 | + "encoding/json" |
| 9 | + "fmt" |
| 10 | + "log" |
| 11 | + "os" |
| 12 | + "path/filepath" |
| 13 | + "sync" |
| 14 | +) |
| 15 | + |
| 16 | +// ToolInfo represents information about a single tool |
| 17 | +type ToolInfo struct { |
| 18 | + Name string `json:"name"` |
| 19 | + Description string `json:"description"` |
| 20 | +} |
| 21 | + |
| 22 | +// ToolsData represents the structure of tools.json |
| 23 | +type ToolsData struct { |
| 24 | + // Map of serverID to array of tools |
| 25 | + Servers map[string][]ToolInfo `json:"servers"` |
| 26 | +} |
| 27 | + |
| 28 | +// ToolsLogger manages logging of MCP server tools to a JSON file |
| 29 | +type ToolsLogger struct { |
| 30 | + logDir string |
| 31 | + fileName string |
| 32 | + data *ToolsData |
| 33 | + mu sync.Mutex |
| 34 | + useFallback bool |
| 35 | +} |
| 36 | + |
| 37 | +var ( |
| 38 | + globalToolsLogger *ToolsLogger |
| 39 | + globalToolsMu sync.RWMutex |
| 40 | +) |
| 41 | + |
| 42 | +// InitToolsLogger initializes the global tools logger |
| 43 | +// If the log directory doesn't exist and can't be created, falls back to no-op |
| 44 | +func InitToolsLogger(logDir, fileName string) error { |
| 45 | + logger, err := initLogger( |
| 46 | + logDir, fileName, os.O_TRUNC, // Truncate existing file to start fresh |
| 47 | + // Setup function: configure the logger after directory is ready |
| 48 | + func(file *os.File, logDir, fileName string) (*ToolsLogger, error) { |
| 49 | + // Close the file immediately - we'll write directly later |
| 50 | + if file != nil { |
| 51 | + file.Close() |
| 52 | + } |
| 53 | + |
| 54 | + tl := &ToolsLogger{ |
| 55 | + logDir: logDir, |
| 56 | + fileName: fileName, |
| 57 | + data: &ToolsData{ |
| 58 | + Servers: make(map[string][]ToolInfo), |
| 59 | + }, |
| 60 | + } |
| 61 | + log.Printf("Tools logging to file: %s", filepath.Join(logDir, fileName)) |
| 62 | + return tl, nil |
| 63 | + }, |
| 64 | + // Error handler: fallback to no-op on error |
| 65 | + func(err error, logDir, fileName string) (*ToolsLogger, error) { |
| 66 | + log.Printf("WARNING: Failed to initialize tools log file: %v", err) |
| 67 | + log.Printf("WARNING: Tools logging disabled") |
| 68 | + tl := &ToolsLogger{ |
| 69 | + logDir: logDir, |
| 70 | + fileName: fileName, |
| 71 | + useFallback: true, |
| 72 | + data: &ToolsData{ |
| 73 | + Servers: make(map[string][]ToolInfo), |
| 74 | + }, |
| 75 | + } |
| 76 | + return tl, nil |
| 77 | + }, |
| 78 | + ) |
| 79 | + |
| 80 | + initGlobalToolsLogger(logger) |
| 81 | + return err |
| 82 | +} |
| 83 | + |
| 84 | +// LogTools logs the tools for a specific server |
| 85 | +func (tl *ToolsLogger) LogTools(serverID string, tools []ToolInfo) error { |
| 86 | + tl.mu.Lock() |
| 87 | + defer tl.mu.Unlock() |
| 88 | + |
| 89 | + if tl.useFallback { |
| 90 | + return nil // Silently skip if in fallback mode |
| 91 | + } |
| 92 | + |
| 93 | + // Update the data structure |
| 94 | + tl.data.Servers[serverID] = tools |
| 95 | + |
| 96 | + // Write the updated data to file |
| 97 | + return tl.writeToFile() |
| 98 | +} |
| 99 | + |
| 100 | +// writeToFile writes the current tools data to the JSON file |
| 101 | +// Caller must hold tl.mu lock |
| 102 | +func (tl *ToolsLogger) writeToFile() error { |
| 103 | + filePath := filepath.Join(tl.logDir, tl.fileName) |
| 104 | + |
| 105 | + // Marshal to JSON with indentation for readability |
| 106 | + jsonData, err := json.MarshalIndent(tl.data, "", " ") |
| 107 | + if err != nil { |
| 108 | + return fmt.Errorf("failed to marshal tools data: %w", err) |
| 109 | + } |
| 110 | + |
| 111 | + // Write to file atomically using a temp file + rename |
| 112 | + tempPath := filePath + ".tmp" |
| 113 | + if err := os.WriteFile(tempPath, jsonData, 0644); err != nil { |
| 114 | + return fmt.Errorf("failed to write temp file: %w", err) |
| 115 | + } |
| 116 | + |
| 117 | + if err := os.Rename(tempPath, filePath); err != nil { |
| 118 | + // Clean up temp file on error |
| 119 | + os.Remove(tempPath) |
| 120 | + return fmt.Errorf("failed to rename temp file: %w", err) |
| 121 | + } |
| 122 | + |
| 123 | + return nil |
| 124 | +} |
| 125 | + |
| 126 | +// Close is a no-op for ToolsLogger (implements closableLogger interface) |
| 127 | +func (tl *ToolsLogger) Close() error { |
| 128 | + // No file handle to close since we write directly each time |
| 129 | + return nil |
| 130 | +} |
| 131 | + |
| 132 | +// Global logging function that uses the global tools logger |
| 133 | + |
| 134 | +// LogToolsForServer logs the tools for a specific server |
| 135 | +func LogToolsForServer(serverID string, tools []ToolInfo) { |
| 136 | + globalToolsMu.RLock() |
| 137 | + defer globalToolsMu.RUnlock() |
| 138 | + |
| 139 | + if globalToolsLogger != nil { |
| 140 | + if err := globalToolsLogger.LogTools(serverID, tools); err != nil { |
| 141 | + // Log errors using the standard logger to avoid recursion |
| 142 | + log.Printf("WARNING: Failed to log tools for server %s: %v", serverID, err) |
| 143 | + } |
| 144 | + } |
| 145 | +} |
| 146 | + |
| 147 | +// CloseToolsLogger closes the global tools logger |
| 148 | +func CloseToolsLogger() error { |
| 149 | + return closeGlobalToolsLogger() |
| 150 | +} |
| 151 | + |
| 152 | +// initGlobalToolsLogger initializes the global ToolsLogger using the generic helper. |
| 153 | +func initGlobalToolsLogger(logger *ToolsLogger) { |
| 154 | + initGlobalLogger(&globalToolsMu, &globalToolsLogger, logger) |
| 155 | +} |
| 156 | + |
| 157 | +// closeGlobalToolsLogger closes the global ToolsLogger using the generic helper. |
| 158 | +func closeGlobalToolsLogger() error { |
| 159 | + return closeGlobalLogger(&globalToolsMu, &globalToolsLogger) |
| 160 | +} |
0 commit comments