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

View File

@@ -1,6 +1,7 @@
import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response';
import { WebUiConfigWrapper } from '../helper/config';
import { logSubscription } from '@/common/log';
// 日志记录
export const LogHandler: RequestHandler = async (req, res) => {
@@ -17,3 +18,17 @@ export const LogListHandler: RequestHandler = async (_, res) => {
const logList = WebUiConfigWrapper.GetLogsList();
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 { LogHandler, LogListHandler } from '../api/Log';
import { LogHandler, LogListHandler, LogRealTimeHandler } from '../api/Log';
const router = Router();
// router:读取日志内容
router.get('/GetLog', LogHandler);
// router:读取日志列表
router.get('/GetLogList', LogListHandler);
// router:实时日志
router.get('/GetLogRealTime', LogRealTimeHandler);
export { router as LogRouter };