jjf123 / src /utils /logWsServer.js
jjf1999's picture
Upload 138 files
ddce7e8 verified
Raw
History Blame Contribute Delete
8.25 kB
/**
* WebSocket 日志服务模块
* 提供实时日志推送和日志文件管理
*/
import { WebSocketServer } from 'ws';
import fs from 'fs';
import path from 'path';
import { getDataDir } from './paths.js';
// 默认配置
const DEFAULT_LOG_MAX_SIZE_MB = 10; // 单个日志文件最大 10MB
const DEFAULT_LOG_MAX_FILES = 5; // 保留 5 个历史文件
const DEFAULT_LOG_MAX_MEMORY = 500; // 内存中保留 500 条日志
// 日志目录
const dataDir = getDataDir();
const LOG_DIR = path.join(dataDir, 'logs');
const LOG_FILE = path.join(LOG_DIR, 'app.log');
// 确保日志目录存在
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
class LogWebSocketServer {
constructor() {
this.wss = null;
this.clients = new Set();
this.logStore = [];
this.currentLogSize = 0;
// 配置(可在运行时更新)
this.maxSizeMB = DEFAULT_LOG_MAX_SIZE_MB;
this.maxFiles = DEFAULT_LOG_MAX_FILES;
this.maxMemory = DEFAULT_LOG_MAX_MEMORY;
// 初始化日志文件大小
this._initLogFileSize();
// 写入缓冲(避免频繁写入)
this.writeBuffer = [];
this.flushTimer = null;
this.FLUSH_INTERVAL = 1000; // 1秒刷新一次
}
/**
* 初始化获取当前日志文件大小
*/
_initLogFileSize() {
try {
if (fs.existsSync(LOG_FILE)) {
const stats = fs.statSync(LOG_FILE);
this.currentLogSize = stats.size;
}
} catch (error) {
this.currentLogSize = 0;
}
}
/**
* 更新配置
*/
updateConfig(config) {
if (config.logMaxSizeMB !== undefined) {
this.maxSizeMB = config.logMaxSizeMB;
}
if (config.logMaxFiles !== undefined) {
this.maxFiles = config.logMaxFiles;
}
if (config.logMaxMemory !== undefined) {
this.maxMemory = config.logMaxMemory;
}
}
/**
* 初始化 WebSocket 服务器
* @param {http.Server} server - HTTP 服务器实例
*/
initialize(server) {
this.wss = new WebSocketServer({ server, path: '/ws/logs' });
this.wss.on('connection', (ws, req) => {
this.clients.add(ws);
// 发送最近的日志历史
const recentLogs = this.logStore.slice(-50);
if (recentLogs.length > 0) {
ws.send(JSON.stringify({
type: 'history',
logs: recentLogs
}));
}
ws.on('close', () => {
this.clients.delete(ws);
});
ws.on('error', () => {
this.clients.delete(ws);
});
});
}
/**
* 广播日志到所有客户端
*/
broadcast(entry) {
const message = JSON.stringify({
type: 'log',
log: entry
});
for (const client of this.clients) {
if (client.readyState === 1) { // OPEN
try {
client.send(message);
} catch (e) {
this.clients.delete(client);
}
}
}
}
/**
* 存储日志条目
*/
storeLog(level, message) {
const entry = {
id: Date.now() + '-' + Math.random().toString(36).substr(2, 9),
timestamp: new Date().toISOString(),
level,
message
};
// 存储到内存
this.logStore.push(entry);
while (this.logStore.length > this.maxMemory) {
this.logStore.shift();
}
// 广播到 WebSocket 客户端
this.broadcast(entry);
// 添加到写入缓冲
this._bufferWrite(entry);
return entry;
}
/**
* 缓冲写入(减少磁盘 I/O)
*/
_bufferWrite(entry) {
const line = `${entry.timestamp} [${entry.level}] ${entry.message}\n`;
this.writeBuffer.push(line);
// 设置定时刷新
if (!this.flushTimer) {
this.flushTimer = setTimeout(() => {
this._flushBuffer();
}, this.FLUSH_INTERVAL);
}
}
/**
* 刷新缓冲到文件
*/
_flushBuffer() {
if (this.writeBuffer.length === 0) {
this.flushTimer = null;
return;
}
const content = this.writeBuffer.join('');
this.writeBuffer = [];
this.flushTimer = null;
const contentSize = Buffer.byteLength(content, 'utf8');
// 检查是否需要轮转
if (this.currentLogSize + contentSize > this.maxSizeMB * 1024 * 1024) {
this._rotateLog();
}
// 追加写入
try {
fs.appendFileSync(LOG_FILE, content, 'utf8');
this.currentLogSize += contentSize;
} catch (error) {
console.error('写入日志文件失败:', error.message);
}
}
/**
* 日志轮转
*/
_rotateLog() {
try {
// 删除最旧的文件
for (let i = this.maxFiles - 1; i >= 1; i--) {
const oldFile = `${LOG_FILE}.${i}`;
const newFile = `${LOG_FILE}.${i + 1}`;
if (fs.existsSync(oldFile)) {
if (i === this.maxFiles - 1) {
fs.unlinkSync(oldFile);
} else {
fs.renameSync(oldFile, newFile);
}
}
}
// 重命名当前文件
if (fs.existsSync(LOG_FILE)) {
fs.renameSync(LOG_FILE, `${LOG_FILE}.1`);
}
this.currentLogSize = 0;
} catch (error) {
console.error('日志轮转失败:', error.message);
}
}
/**
* 获取日志(API 查询)
*/
getLogs(options = {}) {
const { level, search, limit = 100, offset = 0 } = options;
let filtered = [...this.logStore];
// 过滤分隔符
filtered = filtered.filter(log => !this._isSeparator(log.message));
if (level && level !== 'all') {
filtered = filtered.filter(log => log.level === level);
}
if (search) {
const searchLower = search.toLowerCase();
filtered = filtered.filter(log =>
log.message.toLowerCase().includes(searchLower)
);
}
filtered.reverse();
return {
logs: filtered.slice(offset, offset + limit),
total: filtered.length
};
}
/**
* 判断是否为分隔符
*/
_isSeparator(message) {
if (!message || typeof message !== 'string') return false;
const trimmed = message.trim();
if (trimmed.length < 3) return false;
return /^[═─=\-*_~]+$/.test(trimmed);
}
/**
* 清空日志
*/
clearLogs() {
this.logStore.length = 0;
// 广播清空事件
for (const client of this.clients) {
if (client.readyState === 1) {
try {
client.send(JSON.stringify({ type: 'clear' }));
} catch (e) { }
}
}
}
/**
* 获取统计
*/
getLogStats() {
const stats = { total: 0, info: 0, warn: 0, error: 0, request: 0, debug: 0 };
for (const log of this.logStore) {
if (this._isSeparator(log.message)) continue;
stats.total++;
if (stats[log.level] !== undefined) {
stats[log.level]++;
}
}
return stats;
}
/**
* 关闭服务
*/
close() {
// 刷新剩余缓冲
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this._flushBuffer();
}
// 关闭 WebSocket
if (this.wss) {
for (const client of this.clients) {
client.close();
}
this.wss.close();
}
}
}
// 单例
export const logWsServer = new LogWebSocketServer();
export default logWsServer;