UNPKG

@n2flowjs/nbase

Version:

Neural Vector Database for efficient similarity search

417 lines 19.2 kB
"use strict"; // --- START OF FILE vector_monitoring_updated.ts --- var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.VectorDBMonitor = void 0; const os_1 = __importDefault(require("os")); const events_1 = require("events"); const profiling_1 = require("./profiling"); /** * A monitoring utility for tracking system, search, database, and cache metrics * in a vector database environment. This class extends `EventEmitter` to emit * events related to metrics collection, cache hits/misses, and errors. * * The `VectorDBMonitor` collects and maintains metrics such as CPU usage, memory * usage, query performance, database state, and cache efficiency. It provides * methods to start and stop monitoring, record events, and retrieve the current * metrics state and history. * * ### Features: * - Periodic collection of system metrics (CPU, memory, load average). * - Tracking and calculation of search metrics (queries per minute, average response time, P95). * - Monitoring database state (vector count, memory usage). * - Cache hit/miss tracking and hit rate calculation. * - Event-based architecture for emitting metrics snapshots, cache events, and errors. * * ### Usage: * 1. Instantiate the monitor with optional configuration. * 2. Start the monitoring process using `start()`. * 3. Record search and cache events using `recordSearch`, `recordCacheHit`, and `recordCacheMiss`. * 4. Retrieve metrics snapshots via the `metrics` event or `getMetrics()` method. * 5. Stop monitoring using `stop()` when no longer needed. * * @example * ```typescript * const monitor = new VectorDBMonitor({ * interval: 30000, // Collect metrics every 30 seconds * logToConsole: true, // Log metrics snapshots to the console * }); * * monitor.on("metrics", (snapshot) => { * console.log("Metrics Snapshot:", snapshot); * }); * * monitor.start(); * * // Record a search operation * monitor.recordSearch({ * duration: 120, // Response time in ms * method: "vectorSearch", * results: 10, * cacheUsed: 0 * }); * * // Stop monitoring * monitor.stop(); * ``` * * @extends TypedEventEmitter<MonitorEvents> */ class VectorDBMonitor extends events_1.EventEmitter { constructor(options = {}) { super(); this.monitorInterval = null; this.queriesInLastInterval = 0; // Counter for QPM calculation // For CPU calculation this.lastCpuInfo = null; // Resolve options with defaults this.options = { interval: options.interval ?? 60000, // Default: 1 minute historySize: options.historySize ?? 60, // Default: Keep last 60 points (e.g., 1 hour of 1-min intervals) logToConsole: options.logToConsole ?? false, enableSystemMetrics: options.enableSystemMetrics !== false, // Default: true enableSearchMetrics: options.enableSearchMetrics !== false, // Default: true enableDatabaseMetrics: options.enableDatabaseMetrics !== false, // Default: true enableCacheMetrics: options.enableCacheMetrics !== false, // Default: true }; // Initialize metric storage this.metricsHistory = { system: { cpu: [], memory: [], loadAvg1m: [] }, }; this.searchState = { queryCount: 0, averageResponseTime: 0, p95ResponseTime: 0, queriesPerMinute: 0, // Calculated periodically methodUsage: {}, recentResponseTimes: [], // Limited buffer for P95/Avg calculation }; this.databaseState = { vectorCount: 0, memoryUsageBytes: 0, partitionCount: 0, }; this.cacheState = { hits: 0, misses: 0, }; this.timer = (0, profiling_1.createTimer)(); this.startTime = Date.now(); } /** Starts the monitoring interval. */ start() { if (this.monitorInterval) { console.warn('Monitoring is already running.'); return; } console.log(`Starting monitoring (interval: ${this.options.interval}ms)`); // Collect initial metrics immediately this.collectMetrics().catch((err) => console.error('Initial metrics collection failed:', err)); this.monitorInterval = setInterval(() => { this.collectMetrics().catch((err) => console.error('Periodic metrics collection failed:', err)); }, this.options.interval); this.monitorInterval.unref(); // Allow process to exit if this is the only timer } /** Stops the monitoring interval. */ stop() { if (this.monitorInterval) { clearInterval(this.monitorInterval); this.monitorInterval = null; console.log('Monitoring stopped.'); } } /** Collects all enabled metrics and emits a snapshot. */ async collectMetrics() { this.timer.start('collect'); const collectionTimestamp = Date.now(); // --- System Metrics --- let systemMetrics = { cpuUsage: null, memoryUsage: null, loadAvg1m: null, }; if (this.options.enableSystemMetrics) { systemMetrics = this._collectSystemMetrics(); // This now correctly calls _addMetricHistory inside } // --- Search Metrics --- let calculatedSearchMetrics = this._calculateSearchMetrics(); // Calculate QPM, P95 etc. based on current state // --- Database Metrics --- // databaseState is updated externally via updateDatabaseMetrics // --- Cache Metrics --- let cacheMetrics = { hits: 0, misses: 0, hitRate: null, }; if (this.options.enableCacheMetrics) { cacheMetrics = this._calculateCacheMetrics(); } // --- Finalize Snapshot --- const collectionTimeMs = this.timer.stop('collect').total; const uptimeSeconds = (collectionTimestamp - this.startTime) / 1000; const snapshot = { timestamp: new Date(collectionTimestamp).toISOString(), uptimeSeconds: parseFloat(uptimeSeconds.toFixed(2)), collectionTimeMs: parseFloat(collectionTimeMs.toFixed(2)), metrics: { system: systemMetrics, search: { // Use the calculated metrics, not the raw state queryCount: this.searchState.queryCount, // Total count is still relevant averageResponseTime: calculatedSearchMetrics.averageResponseTime, p95ResponseTime: calculatedSearchMetrics.p95ResponseTime, queriesPerMinute: calculatedSearchMetrics.queriesPerMinute, methodUsage: { ...this.searchState.methodUsage }, // Copy method usage }, database: { ...this.databaseState }, // Use current DB state cache: cacheMetrics, // Add cache metrics }, }; // Reset interval-based counters this.queriesInLastInterval = 0; // Emit and Log this.emit('metrics', snapshot); if (this.options.logToConsole) { this._logSnapshot(snapshot); } return snapshot; } /** Collects current system metrics (CPU, Memory, Load) and updates history. */ _collectSystemMetrics() { let cpuUsage = null; let memoryUsage = null; let loadAvg1m = null; try { // Memory Usage const totalMem = os_1.default.totalmem(); const freeMem = os_1.default.freemem(); memoryUsage = totalMem > 0 ? (totalMem - freeMem) / totalMem : null; this._addMetricHistory('memory', memoryUsage); // Load Average const loadAvg = os_1.default.loadavg(); loadAvg1m = loadAvg.length > 0 ? loadAvg[0] : null; this._addMetricHistory('loadAvg1m', loadAvg1m); // Correct key used here // CPU Usage (Interval Calculation) const currentCpuInfo = { time: Date.now(), cpus: os_1.default.cpus() }; if (this.lastCpuInfo && currentCpuInfo.time > this.lastCpuInfo.time) { let totalDiff = 0; let idleDiff = 0; for (let i = 0; i < currentCpuInfo.cpus.length; i++) { const currentCore = currentCpuInfo.cpus[i]; // Ensure the corresponding core exists in the previous snapshot if (this.lastCpuInfo.cpus && i < this.lastCpuInfo.cpus.length) { const lastCore = this.lastCpuInfo.cpus[i]; const currentTotal = Object.values(currentCore.times).reduce((a, b) => a + b, 0); const lastTotal = Object.values(lastCore.times).reduce((a, b) => a + b, 0); totalDiff += currentTotal - lastTotal; idleDiff += currentCore.times.idle - lastCore.times.idle; } else { // Handle cases where the number of CPUs might change or last data is incomplete // For simplicity, we might skip calculation in this edge case or handle it based on requirements console.warn(`CPU core mismatch or missing last data at index ${i}. Skipping diff calculation for this core.`); } } if (totalDiff > 0) { // Prevent division by zero and ensure usage is between 0 and 1 cpuUsage = Math.max(0, Math.min(1, (totalDiff - idleDiff) / totalDiff)); } } this.lastCpuInfo = currentCpuInfo; // Update last info for next interval this._addMetricHistory('cpu', cpuUsage); } catch (error) { console.error('Error collecting system metrics:', error); this.emit('error', { message: 'System metrics collection failed', error, context: 'CollectSystemMetrics', }); // Return nulls on error return { cpuUsage: null, memoryUsage: null, loadAvg1m: null }; } // Return formatted values return { cpuUsage: cpuUsage !== null ? parseFloat(cpuUsage.toFixed(4)) : null, memoryUsage: memoryUsage !== null ? parseFloat(memoryUsage.toFixed(4)) : null, loadAvg1m: loadAvg1m !== null ? parseFloat(loadAvg1m.toFixed(2)) : null, }; } /** * Calculates derived search metrics like QPM and P95 based on current state. * This version calculates QPM based on queries recorded within the monitor interval. */ _calculateSearchMetrics() { let calculatedP95 = 0; let calculatedAvg = this.searchState.averageResponseTime; // Use the rolling average calculated in recordSearch // Calculate P95 from recentResponseTimes buffer if (this.searchState.recentResponseTimes.length > 0) { const sortedTimes = [...this.searchState.recentResponseTimes].sort((a, b) => a - b); const p95Index = Math.max(0, Math.ceil(sortedTimes.length * 0.95) - 1); calculatedP95 = parseFloat((sortedTimes[p95Index] ?? 0).toFixed(2)); } // Calculate QPM based on queries counted in the last interval const intervalSeconds = this.options.interval / 1000; const calculatedQPM = intervalSeconds > 0 ? Math.round((this.queriesInLastInterval / intervalSeconds) * 60) : 0; return { averageResponseTime: calculatedAvg, p95ResponseTime: calculatedP95, queriesPerMinute: calculatedQPM, }; } /** Calculates cache metrics based on current state. */ _calculateCacheMetrics() { const total = this.cacheState.hits + this.cacheState.misses; const hitRate = total > 0 ? parseFloat((this.cacheState.hits / total).toFixed(4)) : null; // Use null for 0/0 case? Or 0? return { hits: this.cacheState.hits, misses: this.cacheState.misses, hitRate: hitRate, }; } /** Logs a metrics snapshot to the console. */ _logSnapshot(snapshot) { const cpu = snapshot.metrics.system.cpuUsage !== null ? `${(snapshot.metrics.system.cpuUsage * 100).toFixed(1)}%` : 'N/A'; const mem = snapshot.metrics.system.memoryUsage !== null ? `${(snapshot.metrics.system.memoryUsage * 100).toFixed(1)}%` : 'N/A'; const load = snapshot.metrics.system.loadAvg1m !== null ? snapshot.metrics.system.loadAvg1m.toFixed(2) : 'N/A'; const qpm = snapshot.metrics.search.queriesPerMinute; const avgT = snapshot.metrics.search.averageResponseTime.toFixed(2); const p95 = snapshot.metrics.search.p95ResponseTime.toFixed(2); const vecCount = snapshot.metrics.database.vectorCount; const cacheHitRate = snapshot.metrics.cache?.hitRate !== null ? `${(snapshot.metrics.cache.hitRate * 100).toFixed(1)}%` : 'N/A'; // Added cache console.log(`[${snapshot.timestamp}] Monitor: ` + `CPU=${cpu} | Mem=${mem} | Load1m=${load} | QPM=${qpm} | ` + `AvgTime=${avgT}ms | P95=${p95}ms | Vectors=${vecCount} | CacheHit=${cacheHitRate}` // Added cache ); } /** Adds a metric value to the history array, trimming if necessary. */ _addMetricHistory(key, value) { // Check if the key is valid for system metrics history if (!(key in this.metricsHistory.system)) { console.warn(`Attempted to add metric to non-existent history key: system.${key}`); return; } if (value === null || value === undefined || isNaN(value)) return; // Don't add invalid values const historyArray = this.metricsHistory.system[key]; historyArray.push(value); // Trim history if (historyArray.length > this.options.historySize) { historyArray.shift(); } } /** Records a completed search operation. */ recordSearch(data) { if (!this.options.enableSearchMetrics || !data) return; const { duration, method } = data; this.searchState.queryCount++; this.queriesInLastInterval++; // Increment counter for QPM calculation // Update method usage this.searchState.methodUsage[method] = (this.searchState.methodUsage[method] || 0) + 1; // Update rolling average and P95 buffer const times = this.searchState.recentResponseTimes; times.push(duration); // Keep buffer size limited (e.g., last 1000 queries for P95/Avg) - Make this configurable? const maxResponseTimeHistory = 1000; if (times.length > maxResponseTimeHistory) { times.shift(); } // Recalculate rolling average (simple moving average over the buffer) const sum = times.reduce((a, b) => a + b, 0); this.searchState.averageResponseTime = parseFloat((sum / times.length).toFixed(2)); // P95 is calculated during collectMetrics } /** Records a cache hit event. */ recordCacheHit() { if (!this.options.enableCacheMetrics) return; this.cacheState.hits++; this.emit('cache:hit', undefined); // Emit event (void payload) } /** Records a cache miss event. */ recordCacheMiss() { if (!this.options.enableCacheMetrics) return; this.cacheState.misses++; this.emit('cache:miss', undefined); // Emit event (void payload) } /** Records a generic event. */ recordEvent(eventType, data) { const event = { type: eventType, timestamp: Date.now(), data, }; this.emit('event', event); if (this.options.logToConsole) { console.log(`[${new Date(event.timestamp).toISOString()}] Monitor Event: ${eventType}`, JSON.stringify(data) // Stringify data for cleaner logging potentially ); } } /** Records an error event. */ recordError(context, error, extraData) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`[${new Date().toISOString()}] Monitor Error [${context}]: ${errorMessage}`, extraData ?? ''); this.emit('error', { message: errorMessage, error: error instanceof Error ? error : undefined, context: context, ...extraData, // Include extra data if provided }); } /** Updates the database-related metrics. Called externally. */ updateDatabaseMetrics(data) { // Use Partial for flexibility if (!this.options.enableDatabaseMetrics || !data) return; if (data.vectorCount !== undefined) { this.databaseState.vectorCount = data.vectorCount; } if (data.memoryUsageBytes !== undefined) { this.databaseState.memoryUsageBytes = data.memoryUsageBytes; } // Add other DB metrics here if needed } /** Gets the current metrics state and history. */ getMetrics() { const currentSearchMetrics = this._calculateSearchMetrics(); // Return a deep copy or a structured object to prevent external modification return { startTime: new Date(this.startTime).toISOString(), options: { ...this.options }, // Shallow copy of options is usually fine history: { system: { // Deep copy history arrays cpu: [...this.metricsHistory.system.cpu], memory: [...this.metricsHistory.system.memory], loadAvg1m: [...this.metricsHistory.system.loadAvg1m], }, }, currentState: { search: { // Use calculated metrics + state that doesn't include raw times queryCount: this.searchState.queryCount, averageResponseTime: currentSearchMetrics.averageResponseTime, p95ResponseTime: currentSearchMetrics.p95ResponseTime, queriesPerMinute: currentSearchMetrics.queriesPerMinute, methodUsage: { ...this.searchState.methodUsage }, }, database: { ...this.databaseState }, // Copy current state cache: { ...this.cacheState }, // Copy current cache state }, }; } getSystemMetrics() { return { platform: os_1.default.platform(), cpuCores: os_1.default.cpus().length, totalMemoryMB: Math.round(os_1.default.totalmem() / (1024 * 1024)), freeMemoryMB: Math.round(os_1.default.freemem() / (1024 * 1024)), nodeVersion: process.version, }; } } exports.VectorDBMonitor = VectorDBMonitor; // --- END OF FILE vector_monitoring_updated.ts --- //# sourceMappingURL=vector_monitoring.js.map