@yinzhouzhi/mcp-filesystem-server
Version:
本地文件系统操作服务器 - 基于STDIO通信的MCP服务,支持文件和目录操作
661 lines (580 loc) • 18 kB
JavaScript
/**
* 日志工具模块
* 提供不同级别的日志记录功能
*/
const fs = require('fs');
const path = require('path');
const util = require('util');
const zlib = require('zlib');
const { pipeline } = require('stream');
// 日志级别定义
const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
NONE: 4
};
// 轮转类型定义
const ROTATION_TYPES = {
SIZE: 'size', // 按大小轮转
DAILY: 'daily', // 每日轮转
WEEKLY: 'weekly', // 每周轮转
MONTHLY: 'monthly' // 每月轮转
};
// 默认日志配置
const defaultConfig = {
level: LOG_LEVELS.INFO,
logToConsole: true,
logToFile: true,
logFilePath: path.join(process.cwd(), 'logs', 'filesystem-server.log'),
maxLogFileSize: 10 * 1024 * 1024, // 10MB
rotateLogFiles: true,
maxLogFiles: 5,
compressRotatedLogs: true, // 压缩轮转的日志文件
rotationType: ROTATION_TYPES.SIZE, // 默认按大小轮转
format: 'text', // 'text' 或 'json'
// 性能优化相关配置
useBuffer: true, // 使用缓冲区
bufferSize: 64 * 1024, // 缓冲区大小 (64KB)
flushInterval: 1000, // 缓冲区刷新间隔 (ms)
asyncCompression: true, // 异步压缩
checkRotationInterval: 60000 // 日志轮转检查间隔 (ms)
};
let config = { ...defaultConfig };
let logFileStream = null;
let lastRotationCheck = Date.now();
let rotationTimer = null;
let logBuffer = ''; // 日志缓冲区
let bufferSize = 0; // 当前缓冲区大小
let flushTimer = null; // 缓冲区刷新定时器
let compressionQueue = []; // 压缩队列
let processingCompression = false; // 是否正在处理压缩
/**
* 初始化日志系统
* @param {Object} options 日志配置
*/
function initialize(options = {}) {
config = { ...defaultConfig, ...options };
// 确保日志目录存在
if (config.logToFile) {
const logDir = path.dirname(config.logFilePath);
if (!fs.existsSync(logDir)) {
try {
fs.mkdirSync(logDir, { recursive: true });
} catch (error) {
console.error(`无法创建日志目录: ${logDir}`, error.message);
config.logToFile = false;
}
}
// 如果已存在日志文件流,关闭它
if (logFileStream) {
logFileStream.end();
logFileStream = null;
}
// 重置缓冲区
logBuffer = '';
bufferSize = 0;
// 取消现有的刷新定时器
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
// 检查是否需要轮转日志
if (config.rotateLogFiles) {
checkRotation(true);
// 设置定期检查日志轮转的定时器
if (rotationTimer) {
clearInterval(rotationTimer);
}
rotationTimer = setInterval(() => {
checkRotation();
}, config.checkRotationInterval);
}
// 创建日志文件流
try {
logFileStream = fs.createWriteStream(config.logFilePath, { flags: 'a' });
// 设置缓冲区刷新定时器
if (config.useBuffer && config.flushInterval > 0) {
flushTimer = setInterval(() => {
flushBuffer();
}, config.flushInterval);
}
} catch (error) {
console.error(`无法创建日志文件: ${config.logFilePath}`, error.message);
config.logToFile = false;
}
}
// 记录初始化信息
info('日志系统初始化完成', {
level: getLevelName(config.level),
logToConsole: config.logToConsole,
logToFile: config.logToFile,
logFilePath: config.logFilePath,
rotateLogFiles: config.rotateLogFiles,
rotationType: config.rotationType,
useBuffer: config.useBuffer,
bufferSize: config.bufferSize,
asyncCompression: config.asyncCompression
});
}
/**
* 刷新日志缓冲区
*/
function flushBuffer() {
if (!config.logToFile || !logFileStream || bufferSize === 0) {
return;
}
try {
// 写入缓冲区内容
logFileStream.write(logBuffer);
// 重置缓冲区
logBuffer = '';
bufferSize = 0;
// 检查是否需要轮转(仅在按大小轮转和刷新缓冲区时)
if (config.rotateLogFiles && config.rotationType === ROTATION_TYPES.SIZE) {
// 只有当距离上次检查已经过了指定间隔时间才进行检查
const now = Date.now();
if (now - lastRotationCheck >= 10000) { // 至少间隔10秒
checkRotation();
}
}
} catch (error) {
console.error('刷新日志缓冲区失败', error.message);
// 出错时尝试重新创建日志流
try {
if (logFileStream) {
logFileStream.end();
}
logFileStream = fs.createWriteStream(config.logFilePath, { flags: 'a' });
} catch (innerError) {
console.error('重新创建日志流失败', innerError.message);
config.logToFile = false;
}
}
}
/**
* 检查是否需要轮转日志
* @param {boolean} force 是否强制检查(忽略时间间隔限制)
*/
function checkRotation(force = false) {
if (!config.rotateLogFiles || !fs.existsSync(config.logFilePath)) {
return;
}
const now = Date.now();
// 如果不是强制检查,且距离上次检查不足指定间隔,则跳过
if (!force && now - lastRotationCheck < config.checkRotationInterval) {
return;
}
lastRotationCheck = now;
let shouldRotate = false;
try {
// 按大小轮转 - 使用stat获取文件大小,避免读取文件内容
if (config.rotationType === ROTATION_TYPES.SIZE) {
const stats = fs.statSync(config.logFilePath);
shouldRotate = stats.size > config.maxLogFileSize;
}
// 按时间轮转
else {
shouldRotate = shouldRotateByTime();
}
if (shouldRotate) {
// 先刷新所有缓冲区内容
flushBuffer();
// 如果有活动的日志流,先关闭它
if (logFileStream) {
logFileStream.end();
logFileStream = null;
}
// 执行轮转
rotateLogFiles();
// 重新创建日志流
logFileStream = fs.createWriteStream(config.logFilePath, { flags: 'a' });
}
} catch (error) {
console.error('检查日志轮转失败', error.message);
}
}
/**
* 根据时间策略判断是否应该轮转日志
* @returns {boolean} 是否应该轮转
*/
function shouldRotateByTime() {
if (!fs.existsSync(config.logFilePath)) {
return false;
}
// 获取文件修改时间
const stats = fs.statSync(config.logFilePath);
const fileDate = new Date(stats.mtime);
const now = new Date();
// 按每日轮转
if (config.rotationType === ROTATION_TYPES.DAILY) {
return fileDate.getDate() !== now.getDate() ||
fileDate.getMonth() !== now.getMonth() ||
fileDate.getFullYear() !== now.getFullYear();
}
// 按周轮转
else if (config.rotationType === ROTATION_TYPES.WEEKLY) {
// 获取一周中的第一天
const getWeekStart = (date) => {
const d = new Date(date);
const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // 调整为从周一开始
return new Date(d.setDate(diff));
};
const currentWeekStart = getWeekStart(now).getTime();
const fileWeekStart = getWeekStart(fileDate).getTime();
return currentWeekStart > fileWeekStart;
}
// 按月轮转
else if (config.rotationType === ROTATION_TYPES.MONTHLY) {
return fileDate.getMonth() !== now.getMonth() ||
fileDate.getFullYear() !== now.getFullYear();
}
return false;
}
/**
* 轮转日志文件
*/
function rotateLogFiles() {
try {
const timestamp = new Date().toISOString()
.replace(/:/g, '-') // 替换冒号为连字符
.replace(/\..+/, '') // 移除毫秒部分
.replace('T', '_'); // 替换T为下划线
// 生成新的轮转文件名(带时间戳)
const rotatedLogPath = `${config.logFilePath}.${timestamp}`;
// 重命名当前日志文件
if (fs.existsSync(config.logFilePath)) {
fs.renameSync(config.logFilePath, rotatedLogPath);
}
// 如果配置了压缩,压缩轮转后的日志文件
if (config.compressRotatedLogs) {
if (config.asyncCompression) {
// 异步压缩 - 添加到压缩队列
compressionQueue.push(rotatedLogPath);
// 如果没有压缩任务在进行,则启动处理
if (!processingCompression) {
processCompressionQueue();
}
} else {
// 同步压缩
compressLogFile(rotatedLogPath);
}
}
// 删除超过maxLogFiles数量的日志文件
cleanupOldLogFiles();
info(`日志已轮转: ${path.basename(rotatedLogPath)}`);
} catch (error) {
console.error('轮转日志文件失败', error.message);
}
}
/**
* 处理压缩队列
*/
async function processCompressionQueue() {
if (compressionQueue.length === 0) {
processingCompression = false;
return;
}
processingCompression = true;
// 获取队列中的下一个文件
const filePath = compressionQueue.shift();
try {
await compressLogFileAsync(filePath);
} catch (error) {
console.error(`压缩日志文件失败: ${filePath}`, error.message);
}
// 处理下一个文件
setTimeout(() => {
processCompressionQueue();
}, 10);
}
/**
* 异步压缩日志文件
* @param {string} filePath 要压缩的文件路径
* @returns {Promise} 压缩完成的Promise
*/
function compressLogFileAsync(filePath) {
return new Promise((resolve, reject) => {
try {
const gzippedPath = `${filePath}.gz`;
// 创建文件流和gzip压缩流
const readStream = fs.createReadStream(filePath);
const writeStream = fs.createWriteStream(gzippedPath);
const gzipStream = zlib.createGzip();
// 使用pipeline进行流操作
pipeline(
readStream,
gzipStream,
writeStream,
(err) => {
if (err) {
reject(err);
return;
}
// 压缩成功后,删除原文件
fs.unlinkSync(filePath);
debug(`日志文件已异步压缩: ${path.basename(gzippedPath)}`);
resolve();
}
);
} catch (error) {
reject(error);
}
});
}
/**
* 压缩指定的日志文件(同步方式)
* @param {string} filePath 要压缩的文件路径
*/
function compressLogFile(filePath) {
try {
const gzippedPath = `${filePath}.gz`;
// 创建文件流和gzip压缩流
const readStream = fs.createReadStream(filePath);
const writeStream = fs.createWriteStream(gzippedPath);
const gzipStream = zlib.createGzip();
// 使用pipeline进行流操作
pipeline(
readStream,
gzipStream,
writeStream,
(err) => {
if (err) {
console.error(`压缩日志文件失败: ${filePath}`, err);
return;
}
// 压缩成功后,删除原文件
fs.unlinkSync(filePath);
debug(`日志文件已压缩: ${path.basename(gzippedPath)}`);
}
);
} catch (error) {
console.error(`压缩日志文件失败: ${filePath}`, error.message);
}
}
/**
* 清理旧的日志文件
*/
function cleanupOldLogFiles() {
try {
// 获取日志目录
const logDir = path.dirname(config.logFilePath);
const baseFileName = path.basename(config.logFilePath);
// 获取所有日志文件 - 使用异步操作读取目录内容
fs.readdir(logDir, (err, files) => {
if (err) {
console.error('读取日志目录失败', err.message);
return;
}
const logFiles = files
.filter(file =>
file.startsWith(baseFileName + '.') &&
(file.endsWith('.gz') || !file.includes('.gz'))
)
.map(file => ({
name: file,
path: path.join(logDir, file),
time: fs.statSync(path.join(logDir, file)).mtime.getTime()
}))
.sort((a, b) => b.time - a.time); // 按时间从新到旧排序
// 如果文件数超过最大值,删除最旧的文件
if (logFiles.length > config.maxLogFiles) {
logFiles.slice(config.maxLogFiles).forEach(file => {
try {
fs.unlinkSync(file.path);
debug(`已删除旧日志文件: ${file.name}`);
} catch (err) {
console.error(`删除旧日志文件失败: ${file.path}`, err.message);
}
});
}
});
} catch (error) {
console.error('清理旧日志文件失败', error.message);
}
}
/**
* 获取日志级别名称
* @param {number} level 日志级别
* @returns {string} 日志级别名称
*/
function getLevelName(level) {
return Object.keys(LOG_LEVELS).find(key => LOG_LEVELS[key] === level) || 'UNKNOWN';
}
/**
* 格式化日志消息
* @param {string} level 日志级别
* @param {string} message 日志消息
* @param {Object} data 附加数据
* @returns {string} 格式化后的日志消息
*/
function formatLogMessage(level, message, data) {
const timestamp = new Date().toISOString();
if (config.format === 'json') {
const logObject = {
timestamp,
level,
message,
...data
};
return JSON.stringify(logObject);
} else {
let formattedData = '';
if (data) {
formattedData = util.inspect(data, { depth: 3, colors: false });
}
return `[${timestamp}] [${level}] ${message} ${formattedData}`;
}
}
/**
* 写入日志
* @param {string} level 日志级别
* @param {string} message 日志消息
* @param {Object} data 附加数据
*/
function log(level, message, data = {}) {
const levelValue = LOG_LEVELS[level];
// 检查是否应该记录此级别的日志
if (levelValue < config.level) {
return;
}
const formattedMessage = formatLogMessage(level, message, data);
// 控制台输出
if (config.logToConsole) {
if (level === 'ERROR') {
console.error(formattedMessage);
} else if (level === 'WARN') {
console.warn(formattedMessage);
} else {
console.log(formattedMessage);
}
}
// 文件输出
if (config.logToFile && logFileStream) {
const messageWithNewline = formattedMessage + '\n';
if (config.useBuffer) {
// 使用缓冲区
logBuffer += messageWithNewline;
bufferSize += messageWithNewline.length;
// 如果缓冲区达到阈值,刷新缓冲区
if (bufferSize >= config.bufferSize) {
flushBuffer();
}
} else {
// 直接写入文件
logFileStream.write(messageWithNewline);
// 检查是否需要轮转(仅在按大小轮转时)
if (config.rotateLogFiles && config.rotationType === ROTATION_TYPES.SIZE) {
checkRotation();
}
}
}
}
/**
* 记录调试级别日志
* @param {string} message 日志消息
* @param {Object} data 附加数据
*/
function debug(message, data = {}) {
log('DEBUG', message, data);
}
/**
* 记录信息级别日志
* @param {string} message 日志消息
* @param {Object} data 附加数据
*/
function info(message, data = {}) {
log('INFO', message, data);
}
/**
* 记录警告级别日志
* @param {string} message 日志消息
* @param {Object} data 附加数据
*/
function warn(message, data = {}) {
log('WARN', message, data);
}
/**
* 记录错误级别日志
* @param {string} message 日志消息
* @param {Object} data 附加数据
*/
function error(message, data = {}) {
log('ERROR', message, data);
}
/**
* 设置日志级别
* @param {string} levelName 日志级别名称
* @returns {boolean} 是否设置成功
*/
function setLevel(levelName) {
const level = LOG_LEVELS[levelName.toUpperCase()];
if (level !== undefined) {
config.level = level;
info(`日志级别已设置为 ${levelName.toUpperCase()}`);
return true;
}
return false;
}
/**
* 设置日志轮转类型
* @param {string} type 轮转类型
* @returns {boolean} 是否设置成功
*/
function setRotationType(type) {
if (ROTATION_TYPES[type.toUpperCase()]) {
config.rotationType = ROTATION_TYPES[type.toUpperCase()];
info(`日志轮转类型已设置为 ${type.toUpperCase()}`);
return true;
}
return false;
}
/**
* 获取当前配置
* @returns {Object} 当前日志配置
*/
function getConfig() {
return { ...config };
}
/**
* 关闭日志系统
*/
function shutdown() {
info('日志系统关闭中');
// 刷新缓冲区
flushBuffer();
// 清除定时器
if (rotationTimer) {
clearInterval(rotationTimer);
rotationTimer = null;
}
if (flushTimer) {
clearInterval(flushTimer);
flushTimer = null;
}
// 关闭日志流
if (logFileStream) {
logFileStream.end();
logFileStream = null;
}
// 确保处理完所有压缩任务
if (compressionQueue.length > 0 && config.asyncCompression) {
console.log(`等待 ${compressionQueue.length} 个日志文件压缩完成...`);
// 不等待压缩完成,直接返回,让进程可以正常退出
}
}
module.exports = {
LOG_LEVELS,
ROTATION_TYPES,
initialize,
debug,
info,
warn,
error,
setLevel,
setRotationType,
getConfig,
shutdown
};