@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
JavaScript
"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