Skip to main content
Glama
log.ts11.1 kB
import { ref, computed } from "vue"; import { defineStore } from "pinia"; import { ElMessage } from "element-plus"; // 日志级别枚举 export enum LogLevel { DEBUG = "debug", INFO = "info", WARN = "warn", ERROR = "error", FATAL = "fatal", } // 日志条目接口 export interface LogEntry { id: string; timestamp: Date; level: LogLevel; source: string; message: string; stack?: string; context?: Record<string, any>; tags?: string[]; } // 日志过滤器接口 export interface LogFilter { levels: LogLevel[]; sources: string[]; searchText: string; startTime?: Date; endTime?: Date; tags: string[]; } // 日志导出选项接口 export interface LogExportOptions { format: "json" | "txt" | "csv"; timeRange: { start: Date; end: Date; }; filters: LogFilter; includeContext: boolean; includeStack: boolean; } // 日志统计信息接口 export interface LogStats { total: number; byLevel: Record<LogLevel, number>; bySource: Record<string, number>; errorRate: number; avgResponseTime?: number; } export const useLogStore = defineStore("log", () => { // 状态 const logs = ref<LogEntry[]>([]); const loading = ref(false); const error = ref<string | null>(null); const isConnected = ref(false); const autoRefresh = ref(true); const refreshInterval = ref(1000); // 1秒 // 过滤器状态 const filters = ref<LogFilter>({ levels: [ LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL, ], sources: [], searchText: "", tags: [], }); // 分页状态 const currentPage = ref(1); const pageSize = ref(100); const totalCount = ref(0); // WebSocket连接 let wsConnection: WebSocket | null = null; let refreshTimer: number | null = null; // 计算属性 const filteredLogs = computed(() => { return logs.value.filter((log) => { // 级别过滤 if (!filters.value.levels.includes(log.level)) { return false; } // 来源过滤 if ( filters.value.sources.length > 0 && !filters.value.sources.includes(log.source) ) { return false; } // 文本搜索 if (filters.value.searchText) { const searchLower = filters.value.searchText.toLowerCase(); if ( !log.message.toLowerCase().includes(searchLower) && !log.source.toLowerCase().includes(searchLower) ) { return false; } } // 标签过滤 if (filters.value.tags.length > 0) { if ( !log.tags || !filters.value.tags.some((tag) => log.tags!.includes(tag)) ) { return false; } } // 时间范围过滤 if (filters.value.startTime && log.timestamp < filters.value.startTime) { return false; } if (filters.value.endTime && log.timestamp > filters.value.endTime) { return false; } return true; }); }); const logStats = computed((): LogStats => { const stats: LogStats = { total: filteredLogs.value.length, byLevel: { [LogLevel.DEBUG]: 0, [LogLevel.INFO]: 0, [LogLevel.WARN]: 0, [LogLevel.ERROR]: 0, [LogLevel.FATAL]: 0, }, bySource: {}, errorRate: 0, }; filteredLogs.value.forEach((log) => { stats.byLevel[log.level]++; stats.bySource[log.source] = (stats.bySource[log.source] || 0) + 1; }); const errorCount = stats.byLevel[LogLevel.ERROR] + stats.byLevel[LogLevel.FATAL]; stats.errorRate = stats.total > 0 ? (errorCount / stats.total) * 100 : 0; return stats; }); const availableSources = computed(() => { const sources = new Set<string>(); logs.value.forEach((log) => sources.add(log.source)); return Array.from(sources).sort(); }); const availableTags = computed(() => { const tags = new Set<string>(); logs.value.forEach((log) => { if (log.tags) { log.tags.forEach((tag) => tags.add(tag)); } }); return Array.from(tags).sort(); }); const paginatedLogs = computed(() => { const start = (currentPage.value - 1) * pageSize.value; const end = start + pageSize.value; return filteredLogs.value.slice(start, end); }); // 方法 const connectWebSocket = () => { if (wsConnection && wsConnection.readyState === WebSocket.OPEN) { return; } try { const wsUrl = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/api/logs/stream`; wsConnection = new WebSocket(wsUrl); wsConnection.onopen = () => { isConnected.value = true; error.value = null; ElMessage.success("日志流连接成功"); }; wsConnection.onmessage = (event) => { try { const logEntry: LogEntry = JSON.parse(event.data); logEntry.timestamp = new Date(logEntry.timestamp); addLogEntry(logEntry); } catch (err) { console.error("解析日志消息失败:", err); } }; wsConnection.onclose = () => { isConnected.value = false; ElMessage.warning("日志流连接已断开"); // 自动重连 if (autoRefresh.value) { setTimeout(connectWebSocket, 3000); } }; wsConnection.onerror = (err) => { error.value = "日志流连接错误"; console.error("WebSocket错误:", err); }; } catch (err) { error.value = "无法创建WebSocket连接"; console.error("WebSocket创建失败:", err); } }; const disconnectWebSocket = () => { if (wsConnection) { wsConnection.close(); wsConnection = null; } isConnected.value = false; }; const addLogEntry = (logEntry: LogEntry) => { logs.value.unshift(logEntry); // 限制内存中的日志数量 if (logs.value.length > 10000) { logs.value = logs.value.slice(0, 8000); } }; const loadHistoryLogs = async (page: number = 1, size: number = 100) => { loading.value = true; error.value = null; try { const params = new URLSearchParams({ page: page.toString(), size: size.toString(), levels: filters.value.levels.join(","), sources: filters.value.sources.join(","), search: filters.value.searchText, tags: filters.value.tags.join(","), }); if (filters.value.startTime) { params.append("startTime", filters.value.startTime.toISOString()); } if (filters.value.endTime) { params.append("endTime", filters.value.endTime.toISOString()); } const response = await fetch(`/api/logs?${params}`); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); // 转换时间戳 const historyLogs = data.logs.map((log: any) => ({ ...log, timestamp: new Date(log.timestamp), })); if (page === 1) { logs.value = historyLogs; } else { logs.value.push(...historyLogs); } totalCount.value = data.total; currentPage.value = page; } catch (err) { error.value = err instanceof Error ? err.message : "加载日志失败"; ElMessage.error(error.value); } finally { loading.value = false; } }; const clearLogs = () => { logs.value = []; totalCount.value = 0; currentPage.value = 1; }; const exportLogs = async (options: LogExportOptions): Promise<Blob> => { const logsToExport = filteredLogs.value.filter((log) => { return ( log.timestamp >= options.timeRange.start && log.timestamp <= options.timeRange.end ); }); let content = ""; switch (options.format) { case "json": content = JSON.stringify( logsToExport.map((log) => ({ ...log, context: options.includeContext ? log.context : undefined, stack: options.includeStack ? log.stack : undefined, })), null, 2, ); break; case "csv": const headers = ["timestamp", "level", "source", "message"]; if (options.includeContext) headers.push("context"); if (options.includeStack) headers.push("stack"); content = [ headers.join(","), ...logsToExport.map((log) => [ log.timestamp.toISOString(), log.level, log.source, `"${log.message.replace(/"/g, '""')}"`, ...(options.includeContext ? [JSON.stringify(log.context || {})] : []), ...(options.includeStack ? [`"${(log.stack || "").replace(/"/g, '""')}"`] : []), ].join(","), ), ].join("\n"); break; case "txt": default: content = logsToExport .map((log) => { let line = `[${log.timestamp.toISOString()}] ${log.level.toUpperCase()} ${log.source}: ${log.message}`; if (options.includeContext && log.context) { line += `\nContext: ${JSON.stringify(log.context, null, 2)}`; } if (options.includeStack && log.stack) { line += `\nStack: ${log.stack}`; } return line; }) .join("\n\n"); break; } const mimeTypes = { json: "application/json", csv: "text/csv", txt: "text/plain", }; return new Blob([content], { type: mimeTypes[options.format] }); }; const updateFilters = (newFilters: Partial<LogFilter>) => { filters.value = { ...filters.value, ...newFilters }; currentPage.value = 1; if (!isConnected.value) { loadHistoryLogs(1, pageSize.value); } }; const setAutoRefresh = (enabled: boolean) => { autoRefresh.value = enabled; if (enabled) { connectWebSocket(); } else { disconnectWebSocket(); } }; const jumpToPage = (page: number) => { if (!isConnected.value) { loadHistoryLogs(page, pageSize.value); } else { currentPage.value = page; } }; // 初始化 const initialize = () => { if (autoRefresh.value) { connectWebSocket(); } else { loadHistoryLogs(1, pageSize.value); } }; // 清理 const cleanup = () => { disconnectWebSocket(); if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } }; return { // 状态 logs, loading, error, isConnected, autoRefresh, refreshInterval, filters, currentPage, pageSize, totalCount, // 计算属性 filteredLogs, logStats, availableSources, availableTags, paginatedLogs, // 方法 connectWebSocket, disconnectWebSocket, loadHistoryLogs, clearLogs, exportLogs, updateFilters, setAutoRefresh, jumpToPage, initialize, cleanup, }; });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/zaizaizhao/mcp-swagger-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server