Skip to main content
Glama
metrics.js7.15 kB
/** * 性能指標收集器 * 收集和分析服務器性能數據 */ /** * 計算百分位數 * @param {number[]} arr - 數值數組 * @param {number} p - 百分位(0-1) * @returns {number} */ function percentile(arr, p) { if (arr.length === 0) return 0; const sorted = [...arr].sort((a, b) => a - b); const index = Math.ceil(sorted.length * p) - 1; return sorted[Math.max(0, index)]; } /** * 性能指標收集器 */ export class MetricsCollector { constructor() { this.startTime = Date.now(); // 請求統計 this.requestCount = 0; this.successCount = 0; this.errorCount = 0; // 持續時間統計(保留最近 1000 個) this.durations = []; this.maxDurations = 1000; // 按工具分類的統計 this.toolMetrics = new Map(); // 緩存統計 this.cacheHits = 0; this.cacheMisses = 0; // 內存統計 this.memorySnapshots = []; this.maxMemorySnapshots = 100; } /** * 記錄請求 * @param {Object} options - 請求選項 * @param {number} options.duration - 請求持續時間(毫秒) * @param {boolean} options.success - 是否成功 * @param {string} options.toolName - 工具名稱 * @param {string} options.conversationId - 對話 ID * @param {Error} options.error - 錯誤對象(如果有) */ recordRequest({ duration, success, toolName, conversationId, error }) { // 總體統計 this.requestCount++; if (success) { this.successCount++; } else { this.errorCount++; } // 記錄持續時間 this.durations.push(duration); if (this.durations.length > this.maxDurations) { this.durations.shift(); } // 按工具統計 if (toolName) { if (!this.toolMetrics.has(toolName)) { this.toolMetrics.set(toolName, { count: 0, successCount: 0, errorCount: 0, durations: [] }); } const toolStats = this.toolMetrics.get(toolName); toolStats.count++; if (success) { toolStats.successCount++; } else { toolStats.errorCount++; } toolStats.durations.push(duration); // 限制每個工具的持續時間記錄數量 if (toolStats.durations.length > 100) { toolStats.durations.shift(); } } } /** * 記錄緩存命中 * @param {boolean} hit - 是否命中 */ recordCacheAccess(hit) { if (hit) { this.cacheHits++; } else { this.cacheMisses++; } } /** * 記錄內存快照 */ recordMemorySnapshot() { const memUsage = process.memoryUsage(); this.memorySnapshots.push({ timestamp: Date.now(), ...memUsage }); if (this.memorySnapshots.length > this.maxMemorySnapshots) { this.memorySnapshots.shift(); } } /** * 獲取整體指標 * @returns {Object} */ getMetrics() { const now = Date.now(); const uptime = now - this.startTime; const uptimeSeconds = Math.floor(uptime / 1000); // 計算請求率 const requestsPerSecond = this.requestCount / (uptimeSeconds || 1); // 計算錯誤率 const errorRate = this.requestCount > 0 ? (this.errorCount / this.requestCount * 100).toFixed(2) : 0; // 計算持續時間統計 const avgDuration = this.durations.length > 0 ? this.durations.reduce((sum, d) => sum + d, 0) / this.durations.length : 0; // 計算緩存命中率 const totalCacheAccess = this.cacheHits + this.cacheMisses; const cacheHitRate = totalCacheAccess > 0 ? (this.cacheHits / totalCacheAccess * 100).toFixed(2) : 0; // 獲取最新內存使用 const latestMemory = this.memorySnapshots.length > 0 ? this.memorySnapshots[this.memorySnapshots.length - 1] : process.memoryUsage(); return { uptime: { ms: uptime, seconds: uptimeSeconds, human: this.formatUptime(uptimeSeconds) }, requests: { total: this.requestCount, success: this.successCount, error: this.errorCount, errorRate: `${errorRate}%`, requestsPerSecond: requestsPerSecond.toFixed(2) }, latency: { avg: avgDuration.toFixed(2), p50: percentile(this.durations, 0.50).toFixed(2), p95: percentile(this.durations, 0.95).toFixed(2), p99: percentile(this.durations, 0.99).toFixed(2), max: this.durations.length > 0 ? Math.max(...this.durations).toFixed(2) : 0, unit: 'ms' }, cache: { hits: this.cacheHits, misses: this.cacheMisses, total: totalCacheAccess, hitRate: `${cacheHitRate}%` }, memory: { rss: `${(latestMemory.rss / 1024 / 1024).toFixed(2)} MB`, heapTotal: `${(latestMemory.heapTotal / 1024 / 1024).toFixed(2)} MB`, heapUsed: `${(latestMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`, external: `${(latestMemory.external / 1024 / 1024).toFixed(2)} MB` }, toolStats: this.getToolStats() }; } /** * 獲取按工具分類的統計 * @returns {Object} */ getToolStats() { const stats = {}; for (const [toolName, data] of this.toolMetrics.entries()) { const errorRate = data.count > 0 ? (data.errorCount / data.count * 100).toFixed(2) : 0; const avgDuration = data.durations.length > 0 ? data.durations.reduce((sum, d) => sum + d, 0) / data.durations.length : 0; stats[toolName] = { count: data.count, success: data.successCount, error: data.errorCount, errorRate: `${errorRate}%`, avgDuration: avgDuration.toFixed(2), p95Duration: percentile(data.durations, 0.95).toFixed(2) }; } return stats; } /** * 格式化運行時間 * @param {number} seconds - 秒數 * @returns {string} */ formatUptime(seconds) { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; const parts = []; if (days > 0) parts.push(`${days}d`); if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); parts.push(`${secs}s`); return parts.join(' '); } /** * 重置所有統計數據 */ reset() { this.requestCount = 0; this.successCount = 0; this.errorCount = 0; this.durations = []; this.toolMetrics.clear(); this.cacheHits = 0; this.cacheMisses = 0; this.memorySnapshots = []; this.startTime = Date.now(); } } /** * 全局指標收集器實例 */ export const globalMetrics = new MetricsCollector(); // 每分鐘記錄一次內存快照 setInterval(() => { globalMetrics.recordMemorySnapshot(); }, 60000); export default MetricsCollector;

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/win10ogod/memory-mcp-server'

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