change logviewer
This commit is contained in:
@@ -142,7 +142,6 @@ export const createServer = (config: any): Server => {
|
|||||||
server.app.get("/api/logs", async (req, reply) => {
|
server.app.get("/api/logs", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const filePath = (req.query as any).file as string;
|
const filePath = (req.query as any).file as string;
|
||||||
const groupByReqId = (req.query as any).groupByReqId === 'true';
|
|
||||||
let logFilePath: string;
|
let logFilePath: string;
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@@ -159,64 +158,8 @@ export const createServer = (config: any): Server => {
|
|||||||
|
|
||||||
const logContent = readFileSync(logFilePath, 'utf8');
|
const logContent = readFileSync(logFilePath, 'utf8');
|
||||||
const logLines = logContent.split('\n').filter(line => line.trim());
|
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聚合
|
return logLines;
|
||||||
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;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get logs:", error);
|
console.error("Failed to get logs:", error);
|
||||||
reply.status(500).send({ error: "Failed to get logs" });
|
reply.status(500).send({ error: "Failed to get logs" });
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface LogViewerProps {
|
|||||||
interface LogEntry {
|
interface LogEntry {
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
level: 'info' | 'warn' | 'error' | 'debug';
|
level: 'info' | 'warn' | 'error' | 'debug';
|
||||||
message: string;
|
message: string; // 现在这个字段直接包含原始JSON字符串
|
||||||
source?: string;
|
source?: string;
|
||||||
reqId?: string;
|
reqId?: string;
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ interface LogGroupSummary {
|
|||||||
logCount: number;
|
logCount: number;
|
||||||
firstLog: string;
|
firstLog: string;
|
||||||
lastLog: string;
|
lastLog: string;
|
||||||
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GroupedLogsResponse {
|
interface GroupedLogsResponse {
|
||||||
@@ -49,7 +50,7 @@ interface GroupedLogsResponse {
|
|||||||
|
|
||||||
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
||||||
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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());
|
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 = {
|
const summary = {
|
||||||
totalRequests: Object.keys(groupedLogs).length,
|
totalRequests: Object.keys(groupedLogs).length,
|
||||||
@@ -117,7 +137,8 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
reqId,
|
reqId,
|
||||||
logCount: groupedLogs[reqId].length,
|
logCount: groupedLogs[reqId].length,
|
||||||
firstLog: groupedLogs[reqId][0]?.timestamp,
|
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);
|
const response = await api.getLogs(selectedFile.path);
|
||||||
|
|
||||||
if (response && Array.isArray(response)) {
|
if (response && Array.isArray(response)) {
|
||||||
const typedLogs: LogEntry[] = response.map(log => ({
|
// 现在接口返回的是原始日志字符串数组,直接存储
|
||||||
...log,
|
setLogs(response);
|
||||||
level: (log.level as 'info' | 'warn' | 'error' | 'debug') || 'info'
|
|
||||||
}));
|
|
||||||
|
|
||||||
setLogs(typedLogs);
|
// 如果启用了分组,使用Web Worker进行聚合(需要转换为LogEntry格式供Worker使用)
|
||||||
|
|
||||||
// 如果启用了分组,使用Web Worker进行聚合
|
|
||||||
if (groupByReqId && workerRef.current) {
|
if (groupByReqId && workerRef.current) {
|
||||||
|
const workerLogs: LogEntry[] = response.map((logLine, index) => ({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: 'info',
|
||||||
|
message: logLine,
|
||||||
|
source: undefined,
|
||||||
|
reqId: undefined
|
||||||
|
}));
|
||||||
|
|
||||||
workerRef.current.postMessage({
|
workerRef.current.postMessage({
|
||||||
type: 'groupLogsByReqId',
|
type: 'groupLogsByReqId',
|
||||||
data: { logs: typedLogs }
|
data: { logs: workerLogs }
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setGroupedLogs(null);
|
setGroupedLogs(null);
|
||||||
@@ -357,17 +382,30 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
if (selectedReqId && groupedLogs.groups[selectedReqId]) {
|
if (selectedReqId && groupedLogs.groups[selectedReqId]) {
|
||||||
return 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 = () => {
|
const downloadLogs = () => {
|
||||||
if (!selectedFile || logs.length === 0) return;
|
if (!selectedFile || logs.length === 0) return;
|
||||||
|
|
||||||
const logText = logs.map(log =>
|
// 直接下载原始日志字符串,每行一个日志
|
||||||
`[${log.timestamp}] [${log.level.toUpperCase()}] ${log.source ? `[${log.source}] ` : ''}${log.message}`
|
const logText = logs.join('\n');
|
||||||
).join('\n');
|
|
||||||
|
|
||||||
const blob = new Blob([logText], { type: 'text/plain' });
|
const blob = new Blob([logText], { type: 'text/plain' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -425,9 +463,15 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
id: 'file',
|
id: 'file',
|
||||||
label: selectedFile.name,
|
label: selectedFile.name,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedReqId(null);
|
if (groupByReqId) {
|
||||||
setGroupedLogs(null);
|
// 如果在分组模式下,点击文件层级应该返回到分组列表
|
||||||
setGroupByReqId(false);
|
setSelectedReqId(null);
|
||||||
|
} else {
|
||||||
|
// 如果不在分组模式下,点击文件层级关闭分组功能
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -465,8 +509,15 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatLogsForEditor = () => {
|
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) {
|
if (!isVisible && !open) {
|
||||||
@@ -610,6 +661,11 @@ export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<File className="h-5 w-5 text-blue-600" />
|
<File className="h-5 w-5 text-blue-600" />
|
||||||
<span className="font-medium text-sm">{request.reqId}</span>
|
<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>
|
</div>
|
||||||
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
{request.logCount} {t('log_viewer.logs')}
|
{request.logCount} {t('log_viewer.logs')}
|
||||||
|
|||||||
@@ -227,8 +227,8 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get logs from specific file
|
// Get logs from specific file
|
||||||
async getLogs(filePath: string): Promise<Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }>> {
|
async getLogs(filePath: string): Promise<string[]> {
|
||||||
return this.get<Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }>>(`/logs?file=${encodeURIComponent(filePath)}`);
|
return this.get<string[]>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear logs from specific file
|
// Clear logs from specific file
|
||||||
|
|||||||
Reference in New Issue
Block a user