stock-bot/apps/stock/web-api/src/services/monitoring.service.ts

356 lines
No EOL
9.3 KiB
TypeScript

/**
* 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<CacheStats> {
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<QueueStats[]> {
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<DatabaseStats[]> {
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<SystemHealth> {
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<ServiceMetrics> {
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<string, any> {
const result: Record<string, any> = {};
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;
}
}