feat: 实时日志 (#584)

* feat: 历史日志

* feat: 实时日志

* fix: EventEmitter实现事件监听
This commit is contained in:
bietiaop 2024-12-01 09:31:47 +08:00 committed by GitHub
parent 040b5535f3
commit d13db5e8eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 26 deletions

View File

@ -3,7 +3,7 @@ import { truncateString } from '@/common/helper';
import path from 'node:path'; import path from 'node:path';
import fs from 'node:fs'; import fs from 'node:fs';
import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core'; import { NTMsgAtType, ChatType, ElementType, MessageElement, RawMessage, SelfInfo } from '@/core';
import EventEmitter from 'node:events';
export enum LogLevel { export enum LogLevel {
DEBUG = 'debug', DEBUG = 'debug',
INFO = 'info', INFO = 'info',
@ -24,6 +24,36 @@ function getFormattedTimestamp() {
return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`; return `${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${milliseconds}`;
} }
const logEmitter = new EventEmitter();
export type LogListener = (msg: string) => void;
class Subscription {
public static MAX_HISTORY = 100;
public static history: string[] = [];
subscribe(listener: LogListener) {
for (const history of Subscription.history) {
try {
listener(history);
} catch (_) {
// ignore
}
}
logEmitter.on('log', listener);
}
unsubscribe(listener: LogListener) {
logEmitter.off('log', listener);
}
notify(msg: string) {
logEmitter.emit('log', msg);
if (Subscription.history.length >= Subscription.MAX_HISTORY) {
Subscription.history.shift();
}
Subscription.history.push(msg);
}
}
export const logSubscription = new Subscription();
export class LogWrapper { export class LogWrapper {
fileLogEnabled = true; fileLogEnabled = true;
consoleLogEnabled = true; consoleLogEnabled = true;
@ -47,7 +77,7 @@ export class LogWrapper {
filename: logPath, filename: logPath,
level: 'debug', level: 'debug',
maxsize: 5 * 1024 * 1024, // 5MB maxsize: 5 * 1024 * 1024, // 5MB
maxFiles: 5 maxFiles: 5,
}), }),
new transports.Console({ new transports.Console({
format: format.combine( format: format.combine(
@ -56,9 +86,9 @@ export class LogWrapper {
const userInfo = meta.userInfo ? `${meta.userInfo} | ` : ''; const userInfo = meta.userInfo ? `${meta.userInfo} | ` : '';
return `${timestamp} [${level}] ${userInfo}${message}`; return `${timestamp} [${level}] ${userInfo}${message}`;
}) })
) ),
}) }),
] ],
}); });
this.setLogSelfInfo({ nick: '', uid: '' }); this.setLogSelfInfo({ nick: '', uid: '' });
@ -72,7 +102,7 @@ export class LogWrapper {
this.logger.error('Failed to read log directory', err); this.logger.error('Failed to read log directory', err);
return; return;
} }
files.forEach(file => { files.forEach((file) => {
const filePath = path.join(logDir, file); const filePath = path.join(logDir, file);
this.deleteOldLogFile(filePath, oneWeekAgo); this.deleteOldLogFile(filePath, oneWeekAgo);
}); });
@ -86,7 +116,7 @@ export class LogWrapper {
return; return;
} }
if (stats.mtime.getTime() < oneWeekAgo) { if (stats.mtime.getTime() < oneWeekAgo) {
fs.unlink(filePath, err => { fs.unlink(filePath, (err) => {
if (err) { if (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
this.logger.warn(`File already deleted: ${filePath}`); this.logger.warn(`File already deleted: ${filePath}`);
@ -111,7 +141,7 @@ export class LogWrapper {
}); });
} }
setLogSelfInfo(selfInfo: { nick: string, uid: string }) { setLogSelfInfo(selfInfo: { nick: string; uid: string }) {
const userInfo = `${selfInfo.nick}`; const userInfo = `${selfInfo.nick}`;
this.logger.defaultMeta = { userInfo }; this.logger.defaultMeta = { userInfo };
} }
@ -135,14 +165,16 @@ export class LogWrapper {
} }
formatMsg(msg: any[]) { formatMsg(msg: any[]) {
return msg.map(msgItem => { return msg
if (msgItem instanceof Error) { .map((msgItem) => {
return msgItem.stack; if (msgItem instanceof Error) {
} else if (typeof msgItem === 'object') { return msgItem.stack;
return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2)))); } else if (typeof msgItem === 'object') {
} return JSON.stringify(truncateString(JSON.parse(JSON.stringify(msgItem, null, 2))));
return msgItem; }
}).join(' '); return msgItem;
})
.join(' ');
} }
_log(level: LogLevel, ...args: any[]) { _log(level: LogLevel, ...args: any[]) {
@ -155,6 +187,7 @@ export class LogWrapper {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, '')); this.logger.log(level, message.replace(/\x1B[@-_][0-?]*[ -/]*[@-~]/g, ''));
} }
logSubscription.notify(message);
} }
log(...args: any[]) { log(...args: any[]) {
@ -282,13 +315,10 @@ function textElementToText(textElement: any): string {
} }
function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string { function replyElementToText(replyElement: any, msg: RawMessage, recursiveLevel: number): string {
const recordMsgOrNull = msg.records.find( const recordMsgOrNull = msg.records.find((record) => replyElement.sourceMsgIdInRecords === record.msgId);
record => replyElement.sourceMsgIdInRecords === record.msgId, return `[回复消息 ${
); recordMsgOrNull && recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020'
return `[回复消息 ${recordMsgOrNull && ? rawMessageToText(recordMsgOrNull, recursiveLevel + 1)
recordMsgOrNull.peerUin != '284840486' && recordMsgOrNull.peerUin != '1094950020' : `未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
?
rawMessageToText(recordMsgOrNull, recursiveLevel + 1) :
`未找到消息记录 (MsgId = ${replyElement.sourceMsgIdInRecords})`
}]`; }]`;
} }

View File

@ -1,6 +1,7 @@
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response'; import { sendError, sendSuccess } from '../utils/response';
import { WebUiConfigWrapper } from '../helper/config'; import { WebUiConfigWrapper } from '../helper/config';
import { logSubscription } from '@/common/log';
// 日志记录 // 日志记录
export const LogHandler: RequestHandler = async (req, res) => { export const LogHandler: RequestHandler = async (req, res) => {
@ -17,3 +18,17 @@ export const LogListHandler: RequestHandler = async (_, res) => {
const logList = WebUiConfigWrapper.GetLogsList(); const logList = WebUiConfigWrapper.GetLogsList();
return sendSuccess(res, logList); return sendSuccess(res, logList);
}; };
// 实时日志SSE
export const LogRealTimeHandler: RequestHandler = async (req, res) => {
const listener = (log: string) => {
try {
res.write(log + '\n');
} catch (error) {
// ignore
}
};
logSubscription.subscribe(listener);
req.on('close', () => {
logSubscription.unsubscribe(listener);
});
};

View File

@ -1,9 +1,13 @@
import { Router } from 'express'; import { Router } from 'express';
import { LogHandler, LogListHandler } from '../api/Log'; import { LogHandler, LogListHandler, LogRealTimeHandler } from '../api/Log';
const router = Router(); const router = Router();
// router:读取日志内容 // router:读取日志内容
router.get('/GetLog', LogHandler); router.get('/GetLog', LogHandler);
// router:读取日志列表 // router:读取日志列表
router.get('/GetLogList', LogListHandler); router.get('/GetLogList', LogListHandler);
// router:实时日志
router.get('/GetLogRealTime', LogRealTimeHandler);
export { router as LogRouter }; export { router as LogRouter };