source dump of claude code
at main 235 lines 6.6 kB view raw
1/** 2 * Error log sink implementation 3 * 4 * This module contains the heavy implementation for error logging and should be 5 * initialized during app startup. It handles file-based error logging to disk. 6 * 7 * Usage: Call initializeErrorLogSink() during app startup to attach the sink. 8 * 9 * DESIGN: This module is separate from log.ts to avoid import cycles. 10 * log.ts has NO heavy dependencies - events are queued until this sink is attached. 11 */ 12 13import axios from 'axios' 14import { dirname, join } from 'path' 15import { getSessionId } from '../bootstrap/state.js' 16import { createBufferedWriter } from './bufferedWriter.js' 17import { CACHE_PATHS } from './cachePaths.js' 18import { registerCleanup } from './cleanupRegistry.js' 19import { logForDebugging } from './debug.js' 20import { getFsImplementation } from './fsOperations.js' 21import { attachErrorLogSink, dateToFilename } from './log.js' 22import { jsonStringify } from './slowOperations.js' 23 24const DATE = dateToFilename(new Date()) 25 26/** 27 * Gets the path to the errors log file. 28 */ 29export function getErrorsPath(): string { 30 return join(CACHE_PATHS.errors(), DATE + '.jsonl') 31} 32 33/** 34 * Gets the path to MCP logs for a server. 35 */ 36export function getMCPLogsPath(serverName: string): string { 37 return join(CACHE_PATHS.mcpLogs(serverName), DATE + '.jsonl') 38} 39 40type JsonlWriter = { 41 write: (obj: object) => void 42 flush: () => void 43 dispose: () => void 44} 45 46function createJsonlWriter(options: { 47 writeFn: (content: string) => void 48 flushIntervalMs?: number 49 maxBufferSize?: number 50}): JsonlWriter { 51 const writer = createBufferedWriter(options) 52 return { 53 write(obj: object): void { 54 writer.write(jsonStringify(obj) + '\n') 55 }, 56 flush: writer.flush, 57 dispose: writer.dispose, 58 } 59} 60 61// Buffered writers for JSONL log files, keyed by path 62const logWriters = new Map<string, JsonlWriter>() 63 64/** 65 * Flush all buffered log writers. Used for testing. 66 * @internal 67 */ 68export function _flushLogWritersForTesting(): void { 69 for (const writer of logWriters.values()) { 70 writer.flush() 71 } 72} 73 74/** 75 * Clear all buffered log writers. Used for testing. 76 * @internal 77 */ 78export function _clearLogWritersForTesting(): void { 79 for (const writer of logWriters.values()) { 80 writer.dispose() 81 } 82 logWriters.clear() 83} 84 85function getLogWriter(path: string): JsonlWriter { 86 let writer = logWriters.get(path) 87 if (!writer) { 88 const dir = dirname(path) 89 writer = createJsonlWriter({ 90 // sync IO: called from sync context 91 writeFn: (content: string) => { 92 try { 93 // Happy-path: directory already exists 94 getFsImplementation().appendFileSync(path, content) 95 } catch { 96 // If any error occurs, assume it was due to missing directory 97 getFsImplementation().mkdirSync(dir) 98 // Retry appending 99 getFsImplementation().appendFileSync(path, content) 100 } 101 }, 102 flushIntervalMs: 1000, 103 maxBufferSize: 50, 104 }) 105 logWriters.set(path, writer) 106 registerCleanup(async () => writer?.dispose()) 107 } 108 return writer 109} 110 111function appendToLog(path: string, message: object): void { 112 if (process.env.USER_TYPE !== 'ant') { 113 return 114 } 115 116 const messageWithTimestamp = { 117 timestamp: new Date().toISOString(), 118 ...message, 119 cwd: getFsImplementation().cwd(), 120 userType: process.env.USER_TYPE, 121 sessionId: getSessionId(), 122 version: MACRO.VERSION, 123 } 124 125 getLogWriter(path).write(messageWithTimestamp) 126} 127 128function extractServerMessage(data: unknown): string | undefined { 129 if (typeof data === 'string') { 130 return data 131 } 132 if (data && typeof data === 'object') { 133 const obj = data as Record<string, unknown> 134 if (typeof obj.message === 'string') { 135 return obj.message 136 } 137 if ( 138 typeof obj.error === 'object' && 139 obj.error && 140 'message' in obj.error && 141 typeof (obj.error as Record<string, unknown>).message === 'string' 142 ) { 143 return (obj.error as Record<string, unknown>).message as string 144 } 145 } 146 return undefined 147} 148 149/** 150 * Implementation for logError - writes error to debug log and file. 151 */ 152function logErrorImpl(error: Error): void { 153 const errorStr = error.stack || error.message 154 155 // Enrich axios errors with request URL, status, and server message for debugging 156 let context = '' 157 if (axios.isAxiosError(error) && error.config?.url) { 158 const parts = [`url=${error.config.url}`] 159 if (error.response?.status !== undefined) { 160 parts.push(`status=${error.response.status}`) 161 } 162 const serverMessage = extractServerMessage(error.response?.data) 163 if (serverMessage) { 164 parts.push(`body=${serverMessage}`) 165 } 166 context = `[${parts.join(',')}] ` 167 } 168 169 logForDebugging(`${error.name}: ${context}${errorStr}`, { level: 'error' }) 170 171 appendToLog(getErrorsPath(), { 172 error: `${context}${errorStr}`, 173 }) 174} 175 176/** 177 * Implementation for logMCPError - writes MCP error to debug log and file. 178 */ 179function logMCPErrorImpl(serverName: string, error: unknown): void { 180 // Not themed, to avoid having to pipe theme all the way down 181 logForDebugging(`MCP server "${serverName}" ${error}`, { level: 'error' }) 182 183 const logFile = getMCPLogsPath(serverName) 184 const errorStr = 185 error instanceof Error ? error.stack || error.message : String(error) 186 187 const errorInfo = { 188 error: errorStr, 189 timestamp: new Date().toISOString(), 190 sessionId: getSessionId(), 191 cwd: getFsImplementation().cwd(), 192 } 193 194 getLogWriter(logFile).write(errorInfo) 195} 196 197/** 198 * Implementation for logMCPDebug - writes MCP debug message to log file. 199 */ 200function logMCPDebugImpl(serverName: string, message: string): void { 201 logForDebugging(`MCP server "${serverName}": ${message}`) 202 203 const logFile = getMCPLogsPath(serverName) 204 205 const debugInfo = { 206 debug: message, 207 timestamp: new Date().toISOString(), 208 sessionId: getSessionId(), 209 cwd: getFsImplementation().cwd(), 210 } 211 212 getLogWriter(logFile).write(debugInfo) 213} 214 215/** 216 * Initialize the error log sink. 217 * 218 * Call this during app startup to attach the error logging backend. 219 * Any errors logged before this is called will be queued and drained. 220 * 221 * Should be called BEFORE initializeAnalyticsSink() in the startup sequence. 222 * 223 * Idempotent: safe to call multiple times (subsequent calls are no-ops). 224 */ 225export function initializeErrorLogSink(): void { 226 attachErrorLogSink({ 227 logError: logErrorImpl, 228 logMCPError: logMCPErrorImpl, 229 logMCPDebug: logMCPDebugImpl, 230 getErrorsPath, 231 getMCPLogsPath, 232 }) 233 234 logForDebugging('Error log sink initialized') 235}