/** * Monitoring Service * Collects health and performance metrics from all system components */ import type { IServiceContainer } from '@stock-bot/handlers'; import { getLogger } from '@stock-bot/logger'; import type { CacheStats, QueueStats, DatabaseStats, SystemHealth, ServiceMetrics, MetricSnapshot } from '../types/monitoring.types'; export class MonitoringService { private readonly logger = getLogger('monitoring-service'); private startTime = Date.now(); constructor(private readonly container: IServiceContainer) {} /** * Get cache/Dragonfly statistics */ async getCacheStats(): Promise { try { if (!this.container.cache) { return { provider: 'dragonfly', connected: false, }; } // Get Redis/Dragonfly info const info = await this.container.cache.info(); const dbSize = await this.container.cache.dbsize(); // Parse memory stats from info const memoryUsed = this.parseInfoValue(info, 'used_memory'); const memoryPeak = this.parseInfoValue(info, 'used_memory_peak'); // Parse stats const hits = this.parseInfoValue(info, 'keyspace_hits'); const misses = this.parseInfoValue(info, 'keyspace_misses'); const evictedKeys = this.parseInfoValue(info, 'evicted_keys'); const expiredKeys = this.parseInfoValue(info, 'expired_keys'); return { provider: 'dragonfly', connected: true, uptime: this.parseInfoValue(info, 'uptime_in_seconds'), memoryUsage: { used: memoryUsed, peak: memoryPeak, }, stats: { hits, misses, keys: dbSize, evictedKeys, expiredKeys, }, info: this.parseRedisInfo(info), }; } catch (error) { this.logger.error('Failed to get cache stats', { error }); return { provider: 'dragonfly', connected: false, }; } } /** * Get queue statistics */ async getQueueStats(): Promise { const stats: QueueStats[] = []; try { if (!this.container.queue) { return stats; } // Get all queue names from the queue manager const queueManager = this.container.queue; const queueNames = ['default', 'proxy', 'qm', 'ib', 'ceo', 'webshare']; // Add your queue names for (const queueName of queueNames) { try { const queue = queueManager.getQueue(queueName); if (!queue) continue; const [waiting, active, completed, failed, delayed, paused] = await Promise.all([ queue.getWaitingCount(), queue.getActiveCount(), queue.getCompletedCount(), queue.getFailedCount(), queue.getDelayedCount(), queue.getPausedCount(), ]); // Get worker info if available const workers = queueManager.getWorker(queueName); const workerInfo = workers ? { count: 1, // Assuming single worker per queue concurrency: workers.concurrency || 1, } : undefined; stats.push({ name: queueName, connected: true, jobs: { waiting, active, completed, failed, delayed, paused, }, workers: workerInfo, }); } catch (error) { this.logger.warn(`Failed to get stats for queue ${queueName}`, { error }); stats.push({ name: queueName, connected: false, jobs: { waiting: 0, active: 0, completed: 0, failed: 0, delayed: 0, paused: 0, }, }); } } } catch (error) { this.logger.error('Failed to get queue stats', { error }); } return stats; } /** * Get database statistics */ async getDatabaseStats(): Promise { const stats: DatabaseStats[] = []; // PostgreSQL stats if (this.container.postgres) { try { const startTime = Date.now(); const result = await this.container.postgres.query('SELECT 1'); const latency = Date.now() - startTime; // Get pool stats const pool = (this.container.postgres as any).pool; const poolStats = pool ? { size: pool.totalCount || 0, active: pool.idleCount || 0, idle: pool.waitingCount || 0, max: pool.options?.max || 0, } : undefined; stats.push({ type: 'postgres', name: 'PostgreSQL', connected: true, latency, pool: poolStats, }); } catch (error) { this.logger.error('Failed to get PostgreSQL stats', { error }); stats.push({ type: 'postgres', name: 'PostgreSQL', connected: false, }); } } // MongoDB stats if (this.container.mongodb) { try { const startTime = Date.now(); const db = this.container.mongodb.db(); await db.admin().ping(); const latency = Date.now() - startTime; const serverStatus = await db.admin().serverStatus(); stats.push({ type: 'mongodb', name: 'MongoDB', connected: true, latency, stats: { version: serverStatus.version, uptime: serverStatus.uptime, connections: serverStatus.connections, opcounters: serverStatus.opcounters, }, }); } catch (error) { this.logger.error('Failed to get MongoDB stats', { error }); stats.push({ type: 'mongodb', name: 'MongoDB', connected: false, }); } } // QuestDB stats if (this.container.questdb) { try { const startTime = Date.now(); // QuestDB health check const response = await fetch(`http://${process.env.QUESTDB_HOST || 'localhost'}:9000/exec?query=SELECT%201`); const latency = Date.now() - startTime; stats.push({ type: 'questdb', name: 'QuestDB', connected: response.ok, latency, }); } catch (error) { this.logger.error('Failed to get QuestDB stats', { error }); stats.push({ type: 'questdb', name: 'QuestDB', connected: false, }); } } return stats; } /** * Get system health summary */ async getSystemHealth(): Promise { const [cacheStats, queueStats, databaseStats] = await Promise.all([ this.getCacheStats(), this.getQueueStats(), this.getDatabaseStats(), ]); const memory = process.memoryUsage(); const uptime = Date.now() - this.startTime; // Determine overall health status const errors: string[] = []; if (!cacheStats.connected) { errors.push('Cache service is disconnected'); } const disconnectedQueues = queueStats.filter(q => !q.connected); if (disconnectedQueues.length > 0) { errors.push(`${disconnectedQueues.length} queue(s) are disconnected`); } const disconnectedDbs = databaseStats.filter(db => !db.connected); if (disconnectedDbs.length > 0) { errors.push(`${disconnectedDbs.length} database(s) are disconnected`); } const status = errors.length === 0 ? 'healthy' : errors.length < 3 ? 'degraded' : 'unhealthy'; return { status, timestamp: new Date().toISOString(), uptime, memory: { used: memory.heapUsed, total: memory.heapTotal, percentage: (memory.heapUsed / memory.heapTotal) * 100, }, services: { cache: cacheStats, queues: queueStats, databases: databaseStats, }, errors: errors.length > 0 ? errors : undefined, }; } /** * Get service metrics (placeholder for future implementation) */ async getServiceMetrics(): Promise { const now = new Date().toISOString(); return { requestsPerSecond: { timestamp: now, value: 0, unit: 'req/s', }, averageResponseTime: { timestamp: now, value: 0, unit: 'ms', }, errorRate: { timestamp: now, value: 0, unit: '%', }, activeConnections: { timestamp: now, value: 0, unit: 'connections', }, }; } /** * Parse value from Redis INFO output */ private parseInfoValue(info: string, key: string): number { const match = info.match(new RegExp(`${key}:(\\d+)`)); return match ? parseInt(match[1], 10) : 0; } /** * Parse Redis INFO into structured object */ private parseRedisInfo(info: string): Record { const result: Record = {}; const sections = info.split('\r\n\r\n'); for (const section of sections) { const lines = section.split('\r\n'); const sectionName = lines[0]?.replace('# ', '') || 'general'; result[sectionName] = {}; for (let i = 1; i < lines.length; i++) { const [key, value] = lines[i].split(':'); if (key && value) { result[sectionName][key] = value; } } } return result; } }