225 lines
5.1 KiB
JavaScript
225 lines
5.1 KiB
JavaScript
|
const morgan = require('morgan');
|
|||
|
const fs = require('fs');
|
|||
|
const path = require('path');
|
|||
|
|
|||
|
// 创建日志目录
|
|||
|
const logDir = path.join(__dirname, '../logs');
|
|||
|
if (!fs.existsSync(logDir)) {
|
|||
|
fs.mkdirSync(logDir, { recursive: true });
|
|||
|
}
|
|||
|
|
|||
|
// 自定义日志格式
|
|||
|
const customFormat = ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time ms';
|
|||
|
|
|||
|
// 创建访问日志流
|
|||
|
const accessLogStream = fs.createWriteStream(
|
|||
|
path.join(logDir, 'access.log'),
|
|||
|
{ flags: 'a' }
|
|||
|
);
|
|||
|
|
|||
|
// 创建错误日志流
|
|||
|
const errorLogStream = fs.createWriteStream(
|
|||
|
path.join(logDir, 'error.log'),
|
|||
|
{ flags: 'a' }
|
|||
|
);
|
|||
|
|
|||
|
/**
|
|||
|
* 请求日志中间件
|
|||
|
*/
|
|||
|
const requestLogger = morgan(customFormat, {
|
|||
|
stream: accessLogStream,
|
|||
|
skip: (req, res) => {
|
|||
|
// 跳过健康检查请求的日志记录
|
|||
|
return req.url === '/api/health';
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* 控制台日志中间件(开发环境)
|
|||
|
*/
|
|||
|
const consoleLogger = morgan('dev', {
|
|||
|
skip: (req, res) => {
|
|||
|
// 生产环境跳过控制台日志
|
|||
|
return process.env.NODE_ENV === 'production';
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* 错误日志中间件
|
|||
|
*/
|
|||
|
const errorLogger = (err, req, res, next) => {
|
|||
|
const timestamp = new Date().toISOString();
|
|||
|
const errorLog = {
|
|||
|
timestamp,
|
|||
|
method: req.method,
|
|||
|
url: req.url,
|
|||
|
ip: req.ip,
|
|||
|
userAgent: req.get('User-Agent'),
|
|||
|
error: {
|
|||
|
name: err.name,
|
|||
|
message: err.message,
|
|||
|
stack: err.stack
|
|||
|
},
|
|||
|
body: req.body,
|
|||
|
params: req.params,
|
|||
|
query: req.query
|
|||
|
};
|
|||
|
|
|||
|
// 写入错误日志文件
|
|||
|
errorLogStream.write(JSON.stringify(errorLog) + '\n');
|
|||
|
|
|||
|
// 控制台输出(开发环境)
|
|||
|
if (process.env.NODE_ENV !== 'production') {
|
|||
|
console.error('错误日志:', errorLog);
|
|||
|
}
|
|||
|
|
|||
|
next(err);
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* 自定义日志记录器
|
|||
|
*/
|
|||
|
class Logger {
|
|||
|
static info(message, data = {}) {
|
|||
|
const logEntry = {
|
|||
|
level: 'INFO',
|
|||
|
timestamp: new Date().toISOString(),
|
|||
|
message,
|
|||
|
data
|
|||
|
};
|
|||
|
|
|||
|
console.log('[INFO]', message, data);
|
|||
|
|
|||
|
// 可选:写入日志文件
|
|||
|
if (process.env.NODE_ENV === 'production') {
|
|||
|
const infoLogStream = fs.createWriteStream(
|
|||
|
path.join(logDir, 'app.log'),
|
|||
|
{ flags: 'a' }
|
|||
|
);
|
|||
|
infoLogStream.write(JSON.stringify(logEntry) + '\n');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
static warn(message, data = {}) {
|
|||
|
const logEntry = {
|
|||
|
level: 'WARN',
|
|||
|
timestamp: new Date().toISOString(),
|
|||
|
message,
|
|||
|
data
|
|||
|
};
|
|||
|
|
|||
|
console.warn('[WARN]', message, data);
|
|||
|
|
|||
|
if (process.env.NODE_ENV === 'production') {
|
|||
|
const warnLogStream = fs.createWriteStream(
|
|||
|
path.join(logDir, 'app.log'),
|
|||
|
{ flags: 'a' }
|
|||
|
);
|
|||
|
warnLogStream.write(JSON.stringify(logEntry) + '\n');
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
static error(message, error = {}, data = {}) {
|
|||
|
const logEntry = {
|
|||
|
level: 'ERROR',
|
|||
|
timestamp: new Date().toISOString(),
|
|||
|
message,
|
|||
|
error: {
|
|||
|
name: error.name,
|
|||
|
message: error.message,
|
|||
|
stack: error.stack
|
|||
|
},
|
|||
|
data
|
|||
|
};
|
|||
|
|
|||
|
console.error('[ERROR]', message, error, data);
|
|||
|
|
|||
|
errorLogStream.write(JSON.stringify(logEntry) + '\n');
|
|||
|
}
|
|||
|
|
|||
|
static debug(message, data = {}) {
|
|||
|
if (process.env.NODE_ENV === 'development') {
|
|||
|
console.debug('[DEBUG]', message, data);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* API调用统计中间件
|
|||
|
*/
|
|||
|
const apiStats = (() => {
|
|||
|
const stats = {
|
|||
|
totalRequests: 0,
|
|||
|
endpoints: {},
|
|||
|
methods: {},
|
|||
|
statusCodes: {},
|
|||
|
averageResponseTime: 0,
|
|||
|
startTime: Date.now()
|
|||
|
};
|
|||
|
|
|||
|
return (req, res, next) => {
|
|||
|
const startTime = Date.now();
|
|||
|
|
|||
|
// 记录请求开始
|
|||
|
stats.totalRequests++;
|
|||
|
|
|||
|
const endpoint = req.route?.path || req.path;
|
|||
|
const method = req.method;
|
|||
|
|
|||
|
stats.endpoints[endpoint] = (stats.endpoints[endpoint] || 0) + 1;
|
|||
|
stats.methods[method] = (stats.methods[method] || 0) + 1;
|
|||
|
|
|||
|
// 监听响应结束
|
|||
|
res.on('finish', () => {
|
|||
|
const responseTime = Date.now() - startTime;
|
|||
|
const statusCode = res.statusCode;
|
|||
|
|
|||
|
stats.statusCodes[statusCode] = (stats.statusCodes[statusCode] || 0) + 1;
|
|||
|
|
|||
|
// 更新平均响应时间
|
|||
|
if (stats.averageResponseTime === 0) {
|
|||
|
stats.averageResponseTime = responseTime;
|
|||
|
} else {
|
|||
|
stats.averageResponseTime = (stats.averageResponseTime + responseTime) / 2;
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
next();
|
|||
|
};
|
|||
|
})();
|
|||
|
|
|||
|
/**
|
|||
|
* 获取API统计信息
|
|||
|
*/
|
|||
|
const getApiStats = () => {
|
|||
|
const uptime = Date.now() - apiStats.startTime;
|
|||
|
return {
|
|||
|
...apiStats,
|
|||
|
uptime,
|
|||
|
uptimeHuman: formatUptime(uptime)
|
|||
|
};
|
|||
|
};
|
|||
|
|
|||
|
/**
|
|||
|
* 格式化运行时间
|
|||
|
*/
|
|||
|
function formatUptime(ms) {
|
|||
|
const seconds = Math.floor(ms / 1000);
|
|||
|
const minutes = Math.floor(seconds / 60);
|
|||
|
const hours = Math.floor(minutes / 60);
|
|||
|
const days = Math.floor(hours / 24);
|
|||
|
|
|||
|
if (days > 0) return `${days}天${hours % 24}小时${minutes % 60}分钟`;
|
|||
|
if (hours > 0) return `${hours}小时${minutes % 60}分钟`;
|
|||
|
if (minutes > 0) return `${minutes}分钟${seconds % 60}秒`;
|
|||
|
return `${seconds}秒`;
|
|||
|
}
|
|||
|
|
|||
|
module.exports = {
|
|||
|
requestLogger,
|
|||
|
consoleLogger,
|
|||
|
errorLogger,
|
|||
|
apiStats,
|
|||
|
getApiStats,
|
|||
|
Logger
|
|||
|
};
|