@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
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 __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