UNPKG

blowback-context

Version:

MCP server that integrates with FE development server for Cursor

273 lines 11.1 kB
import fs from 'fs'; import path from 'path'; import readline from 'readline'; import { LOG_DIRECTORY } from '../constants.js'; import { Logger } from '../utils/logger.js'; /** * Manages log files with rotation implementation */ export class LogManager { static instance; writeStream = null; currentLogCount = 0; MAX_LOGS_PER_FILE = 10000; logDir = LOG_DIRECTORY; currentFileNumber = 0; // Checkpoint related fields checkpointStreams = new Map(); MAX_CHECKPOINT_FILES = 3; constructor() { // Check and create log directory if (!fs.existsSync(this.logDir)) { fs.mkdirSync(this.logDir, { recursive: true }); } this.initializeLogFile(); // Clean up all streams when process exits process.on('beforeExit', () => { this.closeAll(); }); // Handle SIGINT (Ctrl+C) process.on('SIGINT', () => { this.closeAll(); process.exit(0); }); // Handle SIGTERM process.on('SIGTERM', () => { this.closeAll(); process.exit(0); }); } static getInstance() { if (!LogManager.instance) { LogManager.instance = new LogManager(); } return LogManager.instance; } getLogFilePath(fileNumber, checkpointId) { const filename = checkpointId ? `chk-${checkpointId}-${fileNumber}.log` : `default-log-${fileNumber}.log`; return path.join(this.logDir, filename); } initializeLogFile({ checkpointId, nextFileNumber = 0, } = {}) { try { const logFilePath = this.getLogFilePath(nextFileNumber, checkpointId); const writeStream = fs.createWriteStream(logFilePath, { flags: 'a' }); writeStream.on('error', (error) => { Logger.error(`Log file write stream error for ${logFilePath}: ${error}`); }); if (checkpointId) { this.checkpointStreams.get(checkpointId)?.writeStream?.end(); this.checkpointStreams.set(checkpointId, { writeStream, currentFileNumber: nextFileNumber, currentLogCount: 0, timestamp: Date.now(), }); } else { this.writeStream?.end(); this.writeStream = writeStream; this.currentFileNumber = nextFileNumber; this.currentLogCount = 0; } Logger.info(`Initialized log file: ${logFilePath} (${this.currentLogCount} logs)`); } catch (error) { Logger.error(`Failed to initialize log file: ${error}`); } } async attachCheckpointStream(checkpointId) { const detached = this.checkpointStreams.get(checkpointId); if (!detached || detached.writeStream === null) { return; } const writeStream = fs.createWriteStream(this.getLogFilePath(detached.currentFileNumber, checkpointId), { flags: 'a' }); this.checkpointStreams.set(checkpointId, { writeStream, currentFileNumber: detached.currentFileNumber, currentLogCount: detached.currentLogCount, timestamp: Date.now(), }); } isCheckpointStreamAttached(checkpointId) { const stream = this.checkpointStreams.get(checkpointId); return stream && stream.writeStream !== null; } /** * detach checkpoint streams that are not in use */ async detachCheckpointStreams() { const checkpointIds = Array.from(this.checkpointStreams.keys()); checkpointIds.sort((a, b) => a.localeCompare(b)); for (const checkpointId of checkpointIds.slice(0, -this.MAX_CHECKPOINT_FILES)) { const streamData = this.checkpointStreams.get(checkpointId); if (streamData) { streamData.writeStream?.end(); streamData.writeStream = null; } } } async appendLog(logEntry, checkpointId) { try { // Append log to default log file await new Promise((resolve, reject) => { const writeStream = this.writeStream; if (!writeStream) { reject(new Error('Log file is not initialized')); return; } this.writeStream?.write(logEntry, (err) => { if (err) { reject(err); } else { this.currentLogCount++; if (this.currentLogCount >= this.MAX_LOGS_PER_FILE) { this.initializeLogFile({ nextFileNumber: this.currentFileNumber + 1 }); } resolve(); } }); }); // Append log to checkpoint log file if (checkpointId) { if (!this.checkpointStreams.has(checkpointId)) { this.initializeLogFile({ checkpointId }); } else if (this.isCheckpointStreamAttached(checkpointId) === false) { await this.attachCheckpointStream(checkpointId); } this.detachCheckpointStreams(); } if (checkpointId) { await new Promise((resolve, reject) => { const streamData = this.checkpointStreams.get(checkpointId); if (!streamData) { reject(new Error('Checkpoint stream data not found')); return; } streamData.writeStream?.write(logEntry, (err) => { if (err) { reject(err); } else { streamData.currentLogCount++; if (streamData.currentLogCount >= this.MAX_LOGS_PER_FILE) { this.initializeLogFile({ nextFileNumber: streamData.currentFileNumber + 1, checkpointId }); } resolve(); } }); }); } } catch (error) { Logger.error(`Failed to append log: ${error}`); } } async readLogs(limit, checkpointId) { try { // 1. Calculate necessary information const logDir = path.dirname(this.getLogFilePath(0, checkpointId)); const filePattern = checkpointId ? new RegExp(`^chk-${checkpointId}-(\\d+)\\.log$`) : /^default-log-(\d+)\.log$/; // 2. Find log files in directory const files = fs.existsSync(logDir) ? fs.readdirSync(logDir) : []; const logFiles = files .filter(file => filePattern.test(file)) .map(file => { const match = file.match(filePattern); return { file, path: path.join(logDir, file), number: match ? parseInt(match[1], 10) : -1 }; }) .filter(item => item.number >= 0) .sort((a, b) => a.number - b.number); // Sort in order (oldest first) if (logFiles.length === 0) { return { logs: [], writePosition: 0, totalLogs: 0 }; } // 3. Calculate total number of logs (completed files + current file log count) const lastFileIndex = logFiles.length - 1; const completedFilesLogs = lastFileIndex * this.MAX_LOGS_PER_FILE; // Get log count of the last file let currentFileLogCount = 0; if (checkpointId) { const checkpointData = this.checkpointStreams.get(checkpointId); currentFileLogCount = checkpointData ? checkpointData.currentLogCount : 0; } else { currentFileLogCount = this.currentLogCount; } const totalLogs = completedFilesLogs + currentFileLogCount; // 4. Return empty result if no logs needed if (totalLogs === 0) { return { logs: [], writePosition: currentFileLogCount, totalLogs: 0 }; } // 5. Calculate start position and number of logs to read const startPosition = Math.max(0, totalLogs - limit); const startFileIndex = Math.floor(startPosition / this.MAX_LOGS_PER_FILE); const startLogInFile = startPosition % this.MAX_LOGS_PER_FILE; // 6. Read log files (using stream) const logs = []; let logsNeeded = Math.min(limit, totalLogs); for (let i = startFileIndex; i < logFiles.length && logsNeeded > 0; i++) { const filePath = logFiles[i].path; if (!fs.existsSync(filePath)) continue; // Read line by line using readline interface const rl = readline.createInterface({ input: fs.createReadStream(filePath, { encoding: 'utf-8' }), crlfDelay: Infinity }); let skippedLines = 0; // Skip lines if this is the first file and has a start position const shouldSkipLines = (i === startFileIndex && startLogInFile > 0); const linesToSkip = shouldSkipLines ? startLogInFile : 0; for await (const line of rl) { if (!line.trim()) continue; // Skip necessary lines if (shouldSkipLines && skippedLines < linesToSkip) { skippedLines++; continue; } logs.push(line); logsNeeded--; if (logsNeeded <= 0) { rl.close(); break; } } } // 7. Return result return { logs, writePosition: currentFileLogCount, totalLogs }; } catch (error) { Logger.error(`Failed to read logs: ${error}`); return { logs: [], writePosition: 0, totalLogs: 0 }; } } close() { if (this.writeStream) { this.writeStream.end(); this.writeStream = null; } } closeAll() { // Close default log stream this.close(); // Close all checkpoint streams for (const [checkpointId, streamData] of this.checkpointStreams.entries()) { streamData.writeStream?.end(); Logger.info(`Closed checkpoint log stream for ${checkpointId}`); } this.checkpointStreams.clear(); } } //# sourceMappingURL=log-manager.js.map