UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

450 lines 16.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 __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var HorizontalScalingService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.HorizontalScalingService = void 0; const common_1 = require("@nestjs/common"); const rxjs_1 = require("rxjs"); const common_2 = require("@nestjs/common"); let HorizontalScalingService = HorizontalScalingService_1 = class HorizontalScalingService { constructor(telescopeConfig) { this.telescopeConfig = telescopeConfig; this.logger = new common_1.Logger(HorizontalScalingService_1.name); this.nodes = new Map(); this.nodeSubject = new rxjs_1.Subject(); this.healthSubject = new rxjs_1.Subject(); this.loadBalancer = new Map(); this.loadBalancerCounters = new Map(); this.nodeLoadHistory = new Map(); this.heartbeatInterval = null; this.discoveryInterval = null; this.config = this.telescopeConfig.scaling || this.getDefaultScalingConfig(); this.localNode = this.createLocalNode(); } async onModuleInit() { if (!this.config.enabled) { this.logger.log('Horizontal scaling disabled'); return; } await this.initializeScaling(); this.startHeartbeat(); this.startDiscovery(); this.logger.log(`Horizontal scaling initialized for node: ${this.localNode.id}`); } getDefaultScalingConfig() { return { enabled: false, clusterMode: false, nodeId: `node_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, discovery: { method: 'redis', interval: 30000, timeout: 5000, }, loadBalancing: { strategy: 'least-loaded', healthCheckInterval: 10000, failoverEnabled: true, }, dataDistribution: { sharding: false, shardKey: 'timestamp', replicationFactor: 1, consistencyLevel: 'eventual', }, communication: { protocol: 'http', timeout: 5000, retries: 3, compression: true, }, }; } createLocalNode() { return { id: this.config.nodeId, hostname: process.env.HOSTNAME || 'localhost', port: parseInt(process.env.PORT) || 3000, status: 'active', load: 0, memoryUsage: 0, cpuUsage: 0, lastHeartbeat: new Date(), capabilities: ['telescope', 'analytics', 'ml', 'alerting'], version: '10.0.0', }; } async initializeScaling() { this.nodes.set(this.localNode.id, this.localNode); this.nodeSubject.next(this.localNode); this.initializeLoadBalancer(); this.startHealthMonitoring(); } initializeLoadBalancer() { const entryTypes = ['request', 'query', 'exception', 'job', 'cache']; entryTypes.forEach((type) => { this.loadBalancer.set(type, [this.localNode]); }); } startHeartbeat() { this.heartbeatInterval = (0, rxjs_1.interval)(10000).subscribe(async () => { await this.sendHeartbeat(); await this.updateLocalNodeMetrics(); }); } startDiscovery() { this.discoveryInterval = (0, rxjs_1.interval)(this.config.discovery.interval).subscribe(async () => { await this.discoverNodes(); await this.cleanupInactiveNodes(); }); } async sendHeartbeat() { try { this.updateLocalNodeMetrics(); if (this.config.clusterMode) { await this.broadcastHeartbeat(); } this.logger.debug(`Heartbeat sent from node: ${this.localNode.id}`); } catch (error) { this.logger.error(`Failed to send heartbeat: ${error.message}`); } } async updateLocalNodeMetrics() { const memoryUsage = process.memoryUsage(); const cpuUsage = await this.getCpuUsage(); this.localNode.memoryUsage = Math.round(memoryUsage.heapUsed / 1024 / 1024); this.localNode.cpuUsage = cpuUsage; this.localNode.load = this.calculateLoad(); this.localNode.lastHeartbeat = new Date(); const history = this.nodeLoadHistory.get(this.localNode.id) || []; history.push(this.localNode.load); if (history.length > 100) { history.shift(); } this.nodeLoadHistory.set(this.localNode.id, history); this.nodes.set(this.localNode.id, this.localNode); this.nodeSubject.next(this.localNode); } async getCpuUsage() { const startUsage = process.cpuUsage(); await new Promise((resolve) => setTimeout(resolve, 100)); const endUsage = process.cpuUsage(); const userCpu = endUsage.user - startUsage.user; const systemCpu = endUsage.system - startUsage.system; const totalCpu = userCpu + systemCpu; return Math.min(100, (totalCpu / 1000000) * 100); } calculateLoad() { const memoryUsage = process.memoryUsage(); const memoryLoad = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100; return Math.round((memoryLoad + this.localNode.cpuUsage) / 2); } async broadcastHeartbeat() { switch (this.config.communication.protocol) { case 'redis-pubsub': await this.broadcastViaRedis(); break; case 'http': await this.broadcastViaHttp(); break; case 'grpc': await this.broadcastViaGrpc(); break; } } async broadcastViaRedis() { this.logger.debug('Broadcasting heartbeat via Redis'); } async broadcastViaHttp() { for (const node of this.nodes.values()) { if (node.id === this.localNode.id) continue; try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.communication.timeout); const response = await fetch(`http://${node.hostname}:${node.port}/telescope/health`, { method: 'GET', signal: controller.signal, }); clearTimeout(timeoutId); if (response.ok) { const healthData = await response.json(); this.updateNodeHealth(node.id, healthData); } else { this.markNodeUnhealthy(node.id); } } catch (error) { this.logger.warn(`Failed to reach node ${node.id}: ${error.message}`); this.markNodeUnhealthy(node.id); } } } async broadcastViaGrpc() { this.logger.debug('Broadcasting heartbeat via gRPC'); } async discoverNodes() { switch (this.config.discovery.method) { case 'redis': await this.discoverNodesViaRedis(); break; case 'consul': await this.discoverNodesViaConsul(); break; case 'kubernetes': await this.discoverNodesViaKubernetes(); break; case 'manual': break; } } async discoverNodesViaRedis() { this.logger.debug('Discovering nodes via Redis'); } async discoverNodesViaConsul() { this.logger.debug('Discovering nodes via Consul'); } async discoverNodesViaKubernetes() { this.logger.debug('Discovering nodes via Kubernetes'); } updateNodeHealth(nodeId, healthData) { const node = this.nodes.get(nodeId); if (!node) return; node.status = 'active'; node.load = healthData.load || 0; node.memoryUsage = healthData.memoryUsage || 0; node.cpuUsage = healthData.cpuUsage || 0; node.lastHeartbeat = new Date(); this.nodes.set(nodeId, node); this.nodeSubject.next(node); } markNodeUnhealthy(nodeId) { const node = this.nodes.get(nodeId); if (!node) return; node.status = 'unhealthy'; this.nodes.set(nodeId, node); this.nodeSubject.next(node); } async cleanupInactiveNodes() { const now = new Date(); const timeout = this.config.discovery.timeout; for (const [nodeId, node] of this.nodes.entries()) { if (nodeId === this.localNode.id) continue; const timeSinceHeartbeat = now.getTime() - node.lastHeartbeat.getTime(); if (timeSinceHeartbeat > timeout) { this.nodes.delete(nodeId); this.logger.warn(`Removed inactive node: ${nodeId}`); } } } startHealthMonitoring() { (0, rxjs_1.interval)(30000).subscribe(async () => { const health = await this.getClusterHealth(); this.healthSubject.next(health); }); } async routeEntry(entry) { if (!this.config.enabled) { return { targetNode: this.localNode, strategy: 'local', loadFactor: 0, latency: 0, }; } const targetNode = await this.selectTargetNode(entry.type); const loadFactor = targetNode.load; const latency = await this.measureLatency(targetNode); return { targetNode, strategy: this.config.loadBalancing.strategy, loadFactor, latency, }; } async selectTargetNode(entryType) { const availableNodes = this.loadBalancer.get(entryType) || [this.localNode]; const activeNodes = availableNodes.filter((node) => node.status === 'active'); if (activeNodes.length === 0) { return this.localNode; } switch (this.config.loadBalancing.strategy) { case 'round-robin': return this.roundRobinSelection(activeNodes, entryType); case 'least-loaded': return this.leastLoadedSelection(activeNodes); case 'consistent-hash': return this.consistentHashSelection(activeNodes, entryType); case 'random': return this.randomSelection(activeNodes); default: return this.leastLoadedSelection(activeNodes); } } roundRobinSelection(nodes, entryType) { const index = (this.getRoundRobinIndex(entryType) || 0) % nodes.length; return nodes[index]; } getRoundRobinIndex(entryType) { const counter = this.loadBalancerCounters.get(`${entryType}_counter`) || 0; this.loadBalancerCounters.set(`${entryType}_counter`, counter + 1); return counter; } leastLoadedSelection(nodes) { return nodes.reduce((min, node) => (node.load < min.load ? node : min)); } consistentHashSelection(nodes, entryType) { const hash = this.hashString(entryType); const index = hash % nodes.length; return nodes[index]; } hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return Math.abs(hash); } randomSelection(nodes) { const index = Math.floor(Math.random() * nodes.length); return nodes[index]; } async measureLatency(node) { if (node.id === this.localNode.id) { return 0; } const start = Date.now(); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1000); await fetch(`http://${node.hostname}:${node.port}/telescope/health`, { method: 'GET', signal: controller.signal, }); clearTimeout(timeoutId); return Date.now() - start; } catch (error) { return 9999; } } async getClusterHealth() { const allNodes = Array.from(this.nodes.values()); const activeNodes = allNodes.filter((node) => node.status === 'active'); const unhealthyNodes = allNodes.filter((node) => node.status === 'unhealthy'); const averageLoad = activeNodes.length > 0 ? activeNodes.reduce((sum, node) => sum + node.load, 0) / activeNodes.length : 0; const dataDistribution = this.analyzeDataDistribution(); const performance = await this.measureClusterPerformance(); return { totalNodes: allNodes.length, activeNodes: activeNodes.length, unhealthyNodes: unhealthyNodes.length, averageLoad, dataDistribution, performance, }; } analyzeDataDistribution() { const loads = Array.from(this.nodes.values()).map((node) => node.load); const mean = loads.reduce((sum, load) => sum + load, 0) / loads.length; const variance = loads.reduce((sum, load) => sum + Math.pow(load - mean, 2), 0) / loads.length; const standardDeviation = Math.sqrt(variance); const balanced = standardDeviation < 20; const recommendations = []; if (!balanced) { if (standardDeviation > 50) { recommendations.push('High load variance detected. Consider rebalancing cluster.'); } if (loads.some((load) => load > 80)) { recommendations.push('Some nodes are heavily loaded. Consider adding more nodes.'); } } return { balanced, variance: standardDeviation, recommendations, }; } async measureClusterPerformance() { const responseTimes = []; const throughput = this.calculateThroughput(); const errorRate = this.calculateErrorRate(); return { averageResponseTime: responseTimes.length > 0 ? responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length : 0, throughput, errorRate, }; } calculateThroughput() { return 1000; } calculateErrorRate() { return 0.01; } getNodeUpdates() { return this.nodeSubject.asObservable(); } getClusterHealthUpdates() { return this.healthSubject.asObservable(); } getNodes() { return Array.from(this.nodes.values()); } getActiveNodes() { return Array.from(this.nodes.values()).filter((node) => node.status === 'active'); } getNodeById(nodeId) { return this.nodes.get(nodeId); } isLocalNode(nodeId) { return nodeId === this.localNode.id; } getLocalNode() { return this.localNode; } async shutdown() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } if (this.discoveryInterval) { clearInterval(this.discoveryInterval); } if (this.config.clusterMode) { await this.notifyShutdown(); } this.logger.log('Horizontal scaling service shutdown'); } async notifyShutdown() { this.logger.debug('Notifying other nodes about shutdown'); } }; exports.HorizontalScalingService = HorizontalScalingService; exports.HorizontalScalingService = HorizontalScalingService = HorizontalScalingService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_2.Inject)('TELESCOPE_CONFIG')), __metadata("design:paramtypes", [Object]) ], HorizontalScalingService); //# sourceMappingURL=horizontal-scaling.service.js.map