UNPKG

@ahmedhegazee/nestjs-telescope

Version:

Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling

409 lines 17.9 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var ConnectionPoolMonitorService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionPoolMonitorService = void 0; const common_1 = require("@nestjs/common"); const typeorm_1 = require("typeorm"); const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); let ConnectionPoolMonitorService = ConnectionPoolMonitorService_1 = class ConnectionPoolMonitorService { constructor(dataSource) { this.dataSource = dataSource; this.logger = new common_1.Logger(ConnectionPoolMonitorService_1.name); this.destroy$ = new rxjs_1.Subject(); this.metricsSubject = new rxjs_1.BehaviorSubject(this.initializeMetrics()); this.eventsSubject = new rxjs_1.Subject(); this.alertsSubject = new rxjs_1.Subject(); this.connectionEvents = []; this.maxEventHistory = 1000; this.monitoringInterval = 5000; this.acquireTimestamps = new Map(); this.connectionCreationTimes = new Map(); this.alertThresholds = { highUsageThreshold: 80, connectionTimeoutThreshold: 30000, connectionLeakThreshold: 300000, errorRateThreshold: 0.1 }; this.currentMetrics = this.initializeMetrics(); } async onModuleInit() { try { await this.setupMonitoring(); this.startPeriodicMonitoring(); this.logger.log('Connection pool monitoring started'); } catch (error) { this.logger.error('Failed to initialize connection pool monitoring:', error); } } async onModuleDestroy() { this.destroy$.next(); this.destroy$.complete(); this.logger.log('Connection pool monitoring stopped'); } initializeMetrics() { return { totalConnections: 0, activeConnections: 0, idleConnections: 0, waitingConnections: 0, acquiredConnections: 0, releasedConnections: 0, createdConnections: 0, destroyedConnections: 0, poolSize: 0, maxConnections: 0, minConnections: 0, connectionTimeouts: 0, connectionErrors: 0, averageAcquireTime: 0, averageConnectionLifetime: 0, healthScore: 100, lastUpdate: new Date() }; } async setupMonitoring() { const driver = this.dataSource.driver; const pool = driver.pool; if (!pool) { this.logger.warn('Database pool not found, connection monitoring will be limited'); return; } this.setupPoolEventListeners(pool); this.updatePoolConfiguration(pool); } setupPoolEventListeners(pool) { try { const driverType = this.dataSource.options.type; if (driverType === 'postgres') { this.setupPostgresPoolListeners(pool); } else if (driverType === 'mysql') { this.setupMySQLPoolListeners(pool); } else { this.logger.debug(`Connection pool monitoring not fully supported for ${driverType}`); } } catch (error) { this.logger.debug('Could not setup pool event listeners:', error.message); } } setupPostgresPoolListeners(pool) { if (pool.on) { pool.on('connect', (client) => { const connectionId = this.generateConnectionId(); this.connectionCreationTimes.set(connectionId, Date.now()); this.recordEvent({ type: 'create', timestamp: new Date(), connectionId, poolState: this.getCurrentPoolState(pool) }); }); pool.on('acquire', (client) => { const connectionId = this.generateConnectionId(); this.acquireTimestamps.set(connectionId, Date.now()); this.recordEvent({ type: 'acquire', timestamp: new Date(), connectionId, poolState: this.getCurrentPoolState(pool) }); }); pool.on('release', (client) => { const connectionId = this.generateConnectionId(); const acquireTime = this.acquireTimestamps.get(connectionId); const duration = acquireTime ? Date.now() - acquireTime : undefined; this.recordEvent({ type: 'release', timestamp: new Date(), connectionId, duration, poolState: this.getCurrentPoolState(pool) }); this.acquireTimestamps.delete(connectionId); }); pool.on('error', (error, client) => { this.recordEvent({ type: 'error', timestamp: new Date(), error: error.message, poolState: this.getCurrentPoolState(pool) }); }); } } setupMySQLPoolListeners(pool) { if (pool.on) { pool.on('connection', (connection) => { const connectionId = this.generateConnectionId(); this.connectionCreationTimes.set(connectionId, Date.now()); this.recordEvent({ type: 'create', timestamp: new Date(), connectionId, poolState: this.getCurrentPoolState(pool) }); }); pool.on('acquire', (connection) => { const connectionId = this.generateConnectionId(); this.acquireTimestamps.set(connectionId, Date.now()); this.recordEvent({ type: 'acquire', timestamp: new Date(), connectionId, poolState: this.getCurrentPoolState(pool) }); }); pool.on('release', (connection) => { const connectionId = this.generateConnectionId(); const acquireTime = this.acquireTimestamps.get(connectionId); const duration = acquireTime ? Date.now() - acquireTime : undefined; this.recordEvent({ type: 'release', timestamp: new Date(), connectionId, duration, poolState: this.getCurrentPoolState(pool) }); this.acquireTimestamps.delete(connectionId); }); pool.on('error', (error) => { this.recordEvent({ type: 'error', timestamp: new Date(), error: error.message, poolState: this.getCurrentPoolState(pool) }); }); } } getCurrentPoolState(pool) { return { active: pool.acquiredCount || pool._acquiredCount || 0, idle: pool.idleCount || pool._idleCount || 0, waiting: pool.waitingCount || pool._waitingCount || 0 }; } updatePoolConfiguration(pool) { const config = pool.config || pool.options || {}; this.currentMetrics.maxConnections = config.max || config.connectionLimit || 10; this.currentMetrics.minConnections = config.min || 0; this.currentMetrics.poolSize = this.currentMetrics.maxConnections; } startPeriodicMonitoring() { (0, rxjs_1.interval)(this.monitoringInterval) .pipe((0, operators_1.takeUntil)(this.destroy$)) .subscribe(() => { this.collectMetrics(); this.checkForAlerts(); }); } collectMetrics() { const driver = this.dataSource.driver; const pool = driver.pool; if (!pool) { return; } const poolState = this.getCurrentPoolState(pool); this.currentMetrics.activeConnections = poolState.active; this.currentMetrics.idleConnections = poolState.idle; this.currentMetrics.waitingConnections = poolState.waiting; this.currentMetrics.totalConnections = poolState.active + poolState.idle; this.currentMetrics.averageAcquireTime = this.calculateAverageAcquireTime(); this.currentMetrics.averageConnectionLifetime = this.calculateAverageConnectionLifetime(); this.currentMetrics.healthScore = this.calculateHealthScore(); this.currentMetrics.lastUpdate = new Date(); this.updateCountersFromEvents(); this.metricsSubject.next({ ...this.currentMetrics }); } calculateAverageAcquireTime() { const recentEvents = this.connectionEvents .filter(event => event.type === 'release' && event.duration) .slice(-100); if (recentEvents.length === 0) return 0; const totalTime = recentEvents.reduce((sum, event) => sum + (event.duration || 0), 0); return totalTime / recentEvents.length; } calculateAverageConnectionLifetime() { const now = Date.now(); const lifetimes = []; for (const [connectionId, creationTime] of this.connectionCreationTimes) { lifetimes.push(now - creationTime); } if (lifetimes.length === 0) return 0; return lifetimes.reduce((sum, lifetime) => sum + lifetime, 0) / lifetimes.length; } calculateHealthScore() { let score = 100; const utilizationRate = this.currentMetrics.activeConnections / this.currentMetrics.maxConnections; if (utilizationRate > 0.9) { score -= 30; } else if (utilizationRate > 0.8) { score -= 15; } else if (utilizationRate > 0.7) { score -= 5; } if (this.currentMetrics.waitingConnections > 0) { score -= Math.min(20, this.currentMetrics.waitingConnections * 5); } const recentErrors = this.connectionEvents .filter(event => event.type === 'error') .filter(event => Date.now() - event.timestamp.getTime() < 300000); if (recentErrors.length > 0) { score -= Math.min(25, recentErrors.length * 5); } if (this.currentMetrics.averageAcquireTime > 1000) { score -= 10; } return Math.max(0, score); } updateCountersFromEvents() { const now = Date.now(); const recentEvents = this.connectionEvents.filter(event => now - event.timestamp.getTime() < 300000); this.currentMetrics.acquiredConnections = recentEvents.filter(e => e.type === 'acquire').length; this.currentMetrics.releasedConnections = recentEvents.filter(e => e.type === 'release').length; this.currentMetrics.createdConnections = recentEvents.filter(e => e.type === 'create').length; this.currentMetrics.destroyedConnections = recentEvents.filter(e => e.type === 'destroy').length; this.currentMetrics.connectionTimeouts = recentEvents.filter(e => e.type === 'timeout').length; this.currentMetrics.connectionErrors = recentEvents.filter(e => e.type === 'error').length; } checkForAlerts() { const alerts = []; const utilizationRate = this.currentMetrics.activeConnections / this.currentMetrics.maxConnections; if (utilizationRate > this.alertThresholds.highUsageThreshold / 100) { alerts.push({ type: 'high_usage', severity: utilizationRate > 0.95 ? 'critical' : 'high', message: `Connection pool usage is ${(utilizationRate * 100).toFixed(1)}%`, timestamp: new Date(), metrics: { activeConnections: this.currentMetrics.activeConnections, maxConnections: this.currentMetrics.maxConnections }, recommendation: 'Consider increasing pool size or optimizing connection usage' }); } if (this.currentMetrics.waitingConnections > 0) { alerts.push({ type: 'pool_exhausted', severity: this.currentMetrics.waitingConnections > 5 ? 'critical' : 'high', message: `${this.currentMetrics.waitingConnections} connections waiting for availability`, timestamp: new Date(), metrics: { waitingConnections: this.currentMetrics.waitingConnections }, recommendation: 'Pool is exhausted. Consider increasing pool size or investigating connection leaks' }); } const recentErrors = this.connectionEvents .filter(event => event.type === 'error') .filter(event => Date.now() - event.timestamp.getTime() < 60000); if (recentErrors.length > 0) { alerts.push({ type: 'connection_error', severity: recentErrors.length > 5 ? 'critical' : 'medium', message: `${recentErrors.length} connection errors in the last minute`, timestamp: new Date(), metrics: { connectionErrors: recentErrors.length }, recommendation: 'Investigate database connectivity issues or connection configuration' }); } const longRunningConnections = Array.from(this.connectionCreationTimes.values()) .filter(creationTime => Date.now() - creationTime > this.alertThresholds.connectionLeakThreshold) .length; if (longRunningConnections > 0) { alerts.push({ type: 'connection_leak', severity: longRunningConnections > 3 ? 'high' : 'medium', message: `${longRunningConnections} connections have been active for more than 5 minutes`, timestamp: new Date(), metrics: { activeConnections: this.currentMetrics.activeConnections }, recommendation: 'Investigate potential connection leaks in application code' }); } alerts.forEach(alert => { this.alertsSubject.next(alert); this.logger.warn(`Connection pool alert: ${alert.message}`, alert); }); } recordEvent(event) { this.connectionEvents.push(event); if (this.connectionEvents.length > this.maxEventHistory) { this.connectionEvents = this.connectionEvents.slice(-this.maxEventHistory); } this.eventsSubject.next(event); } generateConnectionId() { return `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } getMetrics() { return { ...this.currentMetrics }; } getMetricsStream() { return this.metricsSubject.asObservable().pipe((0, operators_1.shareReplay)(1)); } getEventStream() { return this.eventsSubject.asObservable(); } getAlertStream() { return this.alertsSubject.asObservable(); } getRecentEvents(limit = 100) { return this.connectionEvents.slice(-limit); } getConnectionPoolHealth() { const score = this.currentMetrics.healthScore; const issues = []; const recommendations = []; let status = 'healthy'; if (score < 70) { status = 'critical'; issues.push('Connection pool health is critical'); recommendations.push('Immediate investigation required'); } else if (score < 85) { status = 'warning'; issues.push('Connection pool performance is degraded'); recommendations.push('Monitor closely and consider optimization'); } const utilizationRate = this.currentMetrics.activeConnections / this.currentMetrics.maxConnections; if (utilizationRate > 0.8) { issues.push(`High connection pool utilization: ${(utilizationRate * 100).toFixed(1)}%`); recommendations.push('Consider increasing pool size'); } if (this.currentMetrics.waitingConnections > 0) { issues.push(`${this.currentMetrics.waitingConnections} connections waiting`); recommendations.push('Pool may be undersized for current load'); } return { score, status, issues, recommendations }; } resetMetrics() { this.currentMetrics = this.initializeMetrics(); this.connectionEvents = []; this.acquireTimestamps.clear(); this.connectionCreationTimes.clear(); this.metricsSubject.next(this.currentMetrics); } }; exports.ConnectionPoolMonitorService = ConnectionPoolMonitorService; exports.ConnectionPoolMonitorService = ConnectionPoolMonitorService = ConnectionPoolMonitorService_1 = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [typeorm_1.DataSource]) ], ConnectionPoolMonitorService); //# sourceMappingURL=connection-pool-monitor.service.js.map