change logviewer

This commit is contained in:
musistudio
2025-09-06 22:19:40 +08:00
parent 1a7e90df39
commit cec8421dd9
3 changed files with 80 additions and 81 deletions

View File

@@ -142,7 +142,6 @@ export const createServer = (config: any): Server => {
server.app.get("/api/logs", async (req, reply) => {
try {
const filePath = (req.query as any).file as string;
const groupByReqId = (req.query as any).groupByReqId === 'true';
let logFilePath: string;
if (filePath) {
@@ -160,63 +159,7 @@ export const createServer = (config: any): Server => {
const logContent = readFileSync(logFilePath, 'utf8');
const logLines = logContent.split('\n').filter(line => line.trim());
const logs = logLines.map(line => {
try {
// 尝试解析JSON格式的日志
const logEntry = JSON.parse(line);
return {
timestamp: logEntry.timestamp || logEntry.time || new Date(logEntry.time).toISOString() || new Date().toISOString(),
level: logEntry.level || 'info',
message: logEntry.message || logEntry.msg || line,
source: logEntry.source || undefined,
reqId: logEntry.reqId || undefined
};
} catch {
// 如果不是JSON格式创建一个基本的日志条目
return {
timestamp: new Date().toISOString(),
level: 'info',
message: line,
source: undefined,
reqId: undefined
};
}
});
// 如果需要按reqId聚合
if (groupByReqId) {
const groupedLogs: { [reqId: string]: typeof logs } = {};
logs.forEach(log => {
const reqId = log.reqId || 'no-req-id';
if (!groupedLogs[reqId]) {
groupedLogs[reqId] = [];
}
groupedLogs[reqId].push(log);
});
// 按时间戳排序每个组的日志
Object.keys(groupedLogs).forEach(reqId => {
groupedLogs[reqId].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
});
return {
grouped: true,
groups: groupedLogs,
summary: {
totalRequests: Object.keys(groupedLogs).length,
totalLogs: logs.length,
requests: Object.keys(groupedLogs).map(reqId => ({
reqId,
logCount: groupedLogs[reqId].length,
firstLog: groupedLogs[reqId][0]?.timestamp,
lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.timestamp
}))
}
};
}
return logs;
return logLines;
} catch (error) {
console.error("Failed to get logs:", error);
reply.status(500).send({ error: "Failed to get logs" });

View File

@@ -14,7 +14,7 @@ interface LogViewerProps {
interface LogEntry {
timestamp: string;
level: 'info' | 'warn' | 'error' | 'debug';
message: string;
message: string; // 现在这个字段直接包含原始JSON字符串
source?: string;
reqId?: string;
}
@@ -35,6 +35,7 @@ interface LogGroupSummary {
logCount: number;
firstLog: string;
lastLog: string;
model?: string;
}
interface GroupedLogsResponse {
@@ -49,7 +50,7 @@ interface GroupedLogsResponse {
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
const { t } = useTranslation();
const [logs, setLogs] = useState<LogEntry[]>([]);
const [logs, setLogs] = useState<string[]>([]);
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
const [isLoading, setIsLoading] = useState(false);
@@ -109,6 +110,25 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
groupedLogs[reqId].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
});
// 提取model信息
const extractModelInfo = (reqId) => {
const logGroup = groupedLogs[reqId];
for (const log of logGroup) {
try {
// 尝试从message字段解析JSON
if (log.message && log.message.startsWith('{')) {
const messageObj = JSON.parse(log.message);
if (messageObj.body && messageObj.body.model) {
return messageObj.body.model;
}
}
} catch (e) {
// 解析失败,继续尝试下一条日志
}
}
return undefined;
};
// 生成摘要信息
const summary = {
totalRequests: Object.keys(groupedLogs).length,
@@ -117,7 +137,8 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
reqId,
logCount: groupedLogs[reqId].length,
firstLog: groupedLogs[reqId][0]?.timestamp,
lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.timestamp
lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.timestamp,
model: extractModelInfo(reqId)
}))
};
@@ -272,18 +293,22 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
const response = await api.getLogs(selectedFile.path);
if (response && Array.isArray(response)) {
const typedLogs: LogEntry[] = response.map(log => ({
...log,
level: (log.level as 'info' | 'warn' | 'error' | 'debug') || 'info'
// 现在接口返回的是原始日志字符串数组,直接存储
setLogs(response);
// 如果启用了分组使用Web Worker进行聚合需要转换为LogEntry格式供Worker使用
if (groupByReqId && workerRef.current) {
const workerLogs: LogEntry[] = response.map((logLine, index) => ({
timestamp: new Date().toISOString(),
level: 'info',
message: logLine,
source: undefined,
reqId: undefined
}));
setLogs(typedLogs);
// 如果启用了分组使用Web Worker进行聚合
if (groupByReqId && workerRef.current) {
workerRef.current.postMessage({
type: 'groupLogsByReqId',
data: { logs: typedLogs }
data: { logs: workerLogs }
});
} else {
setGroupedLogs(null);
@@ -357,17 +382,30 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
if (selectedReqId && groupedLogs.groups[selectedReqId]) {
return groupedLogs.groups[selectedReqId];
}
return logs;
// 当在分组模式但没有选中具体请求时,显示原始日志字符串数组
return logs.map(logLine => ({
timestamp: new Date().toISOString(),
level: 'info',
message: logLine,
source: undefined,
reqId: undefined
}));
}
return logs;
// 当不在分组模式时,显示原始日志字符串数组
return logs.map(logLine => ({
timestamp: new Date().toISOString(),
level: 'info',
message: logLine,
source: undefined,
reqId: undefined
}));
};
const downloadLogs = () => {
if (!selectedFile || logs.length === 0) return;
const logText = logs.map(log =>
`[${log.timestamp}] [${log.level.toUpperCase()}] ${log.source ? `[${log.source}] ` : ''}${log.message}`
).join('\n');
// 直接下载原始日志字符串,每行一个日志
const logText = logs.join('\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
@@ -425,10 +463,16 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
id: 'file',
label: selectedFile.name,
onClick: () => {
if (groupByReqId) {
// 如果在分组模式下,点击文件层级应该返回到分组列表
setSelectedReqId(null);
} else {
// 如果不在分组模式下,点击文件层级关闭分组功能
setSelectedReqId(null);
setGroupedLogs(null);
setGroupByReqId(false);
}
}
});
}
@@ -465,8 +509,15 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
};
const formatLogsForEditor = () => {
const displayLogs = getDisplayLogs();
return JSON.stringify(displayLogs, null, 2);
// 如果在分组模式且选中了具体请求,显示该请求的日志
if (groupByReqId && groupedLogs && selectedReqId && groupedLogs.groups[selectedReqId]) {
const requestLogs = groupedLogs.groups[selectedReqId];
// 提取原始JSON字符串并每行一个
return requestLogs.map(log => log.message).join('\n');
}
// 其他情况,直接显示原始日志字符串数组,每行一个
return logs.join('\n');
};
if (!isVisible && !open) {
@@ -610,6 +661,11 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
<div className="flex items-center gap-2">
<File className="h-5 w-5 text-blue-600" />
<span className="font-medium text-sm">{request.reqId}</span>
{request.model && (
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
{request.model}
</span>
)}
</div>
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
{request.logCount} {t('log_viewer.logs')}

View File

@@ -227,8 +227,8 @@ class ApiClient {
}
// Get logs from specific file
async getLogs(filePath: string): Promise<Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }>> {
return this.get<Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }>>(`/logs?file=${encodeURIComponent(filePath)}`);
async getLogs(filePath: string): Promise<string[]> {
return this.get<string[]>(`/logs?file=${encodeURIComponent(filePath)}`);
}
// Clear logs from specific file