UNPKG

@ahmedhegazee/nestjs-telescope

Version:

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

601 lines 22.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); 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 __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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 MultiTenantService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiTenantService = void 0; const common_1 = require("@nestjs/common"); const rxjs_1 = require("rxjs"); const common_2 = require("@nestjs/common"); const crypto = __importStar(require("crypto")); let MultiTenantService = MultiTenantService_1 = class MultiTenantService { constructor(telescopeConfig) { this.telescopeConfig = telescopeConfig; this.logger = new common_1.Logger(MultiTenantService_1.name); this.tenants = new Map(); this.tenantMetrics = new Map(); this.quotaExceeded = new Map(); this.tenantSubject = new rxjs_1.Subject(); this.metricsSubject = new rxjs_1.Subject(); this.quotaSubject = new rxjs_1.Subject(); this.monitoringInterval = null; this.config = this.telescopeConfig.multiTenant || this.getDefaultMultiTenantConfig(); } async onModuleInit() { if (!this.config.enabled) { this.logger.log('Multi-tenant support disabled'); return; } await this.initializeMultiTenancy(); this.startMonitoring(); this.logger.log('Multi-tenant service initialized'); } getDefaultMultiTenantConfig() { return { enabled: true, isolation: { strategy: 'database', databasePrefix: 'telescope_', schemaPrefix: 'tenant_', }, management: { autoProvisioning: true, resourceLimits: true, billing: false, quotas: { storage: 1024, requests: 100000, users: 10, watchers: 5, }, }, features: { customBranding: true, customThemes: true, customConfigurations: true, whiteLabel: false, }, security: { tenantIsolation: true, crossTenantAccess: false, dataEncryption: true, }, }; } async initializeMultiTenancy() { await this.createDefaultTenant(); if (this.config.security.tenantIsolation) { await this.initializeTenantIsolation(); } if (this.config.management.resourceLimits) { await this.initializeResourceMonitoring(); } } async createDefaultTenant() { const defaultTenant = { id: 'default', name: 'Default Tenant', slug: 'default', status: 'active', plan: 'enterprise', createdAt: new Date(), updatedAt: new Date(), metadata: { industry: 'technology', size: 'enterprise', region: 'global', timezone: 'UTC', language: 'en', }, configuration: this.getDefaultConfiguration(), limits: this.getDefaultLimits(), usage: this.getDefaultUsage(), branding: this.getDefaultBranding(), }; this.tenants.set(defaultTenant.id, defaultTenant); this.logger.log('Default tenant created'); } getDefaultConfiguration() { return { features: { requestWatcher: true, queryWatcher: true, exceptionWatcher: true, jobWatcher: true, cacheWatcher: true, mlAnalytics: true, alerting: true, dashboard: true, }, settings: { dataRetention: 90, samplingRate: 100, alertChannels: ['email'], notificationPreferences: { email: true, slack: false, webhook: false, }, }, integrations: {}, }; } getDefaultLimits() { return { storage: this.config.management.quotas.storage, requests: this.config.management.quotas.requests, users: this.config.management.quotas.users, watchers: this.config.management.quotas.watchers, apiCalls: 10000, customFields: 50, retentionDays: 90, }; } getDefaultUsage() { return { storage: 0, requests: 0, users: 1, watchers: 0, apiCalls: 0, customFields: 0, lastUpdated: new Date(), }; } getDefaultBranding() { return { theme: 'light', }; } async initializeTenantIsolation() { this.logger.log(`Initializing tenant isolation with strategy: ${this.config.isolation.strategy}`); switch (this.config.isolation.strategy) { case 'database': await this.initializeDatabaseIsolation(); break; case 'schema': await this.initializeSchemaIsolation(); break; case 'row': await this.initializeRowIsolation(); break; case 'application': await this.initializeApplicationIsolation(); break; } } async initializeDatabaseIsolation() { this.logger.log('Database isolation initialized'); } async initializeSchemaIsolation() { this.logger.log('Schema isolation initialized'); } async initializeRowIsolation() { this.logger.log('Row isolation initialized'); } async initializeApplicationIsolation() { this.logger.log('Application isolation initialized'); } async initializeResourceMonitoring() { this.logger.log('Resource monitoring initialized'); } startMonitoring() { this.monitoringInterval = (0, rxjs_1.interval)(300000).subscribe(async () => { await this.monitorTenantResources(); }); } async provisionTenant(tenantData) { try { this.logger.log(`Provisioning tenant: ${tenantData.name}`); if (this.tenants.has(tenantData.slug)) { return { success: false, error: 'Tenant slug already exists', }; } const tenant = { id: crypto.randomUUID(), name: tenantData.name, slug: tenantData.slug, domain: tenantData.domain, status: 'pending', plan: tenantData.plan, createdAt: new Date(), updatedAt: new Date(), metadata: tenantData.metadata || {}, configuration: this.getConfigurationForPlan(tenantData.plan), limits: this.getLimitsForPlan(tenantData.plan), usage: this.getDefaultUsage(), branding: this.getDefaultBranding(), }; const resources = await this.provisionTenantResources(tenant); tenant.status = 'active'; this.tenants.set(tenant.id, tenant); this.tenantSubject.next(tenant); this.logger.log(`Tenant provisioned successfully: ${tenant.id}`); return { success: true, tenant, resources, }; } catch (error) { this.logger.error(`Failed to provision tenant: ${error.message}`); return { success: false, error: error.message, }; } } getConfigurationForPlan(plan) { const baseConfig = this.getDefaultConfiguration(); switch (plan) { case 'free': return { ...baseConfig, features: { ...baseConfig.features, mlAnalytics: false, alerting: false, }, settings: { ...baseConfig.settings, dataRetention: 30, samplingRate: 10, }, }; case 'basic': return { ...baseConfig, features: { ...baseConfig.features, mlAnalytics: false, }, settings: { ...baseConfig.settings, dataRetention: 60, samplingRate: 50, }, }; case 'professional': return baseConfig; case 'enterprise': return { ...baseConfig, features: { ...baseConfig.features, }, settings: { ...baseConfig.settings, dataRetention: 365, samplingRate: 100, }, }; default: return baseConfig; } } getLimitsForPlan(plan) { const baseLimits = this.getDefaultLimits(); switch (plan) { case 'free': return { ...baseLimits, storage: 100, requests: 1000, users: 1, watchers: 2, apiCalls: 100, customFields: 5, retentionDays: 30, }; case 'basic': return { ...baseLimits, storage: 512, requests: 10000, users: 5, watchers: 3, apiCalls: 1000, customFields: 20, retentionDays: 60, }; case 'professional': return baseLimits; case 'enterprise': return { ...baseLimits, storage: 10240, requests: 1000000, users: 100, watchers: 20, apiCalls: 100000, customFields: 200, retentionDays: 365, }; default: return baseLimits; } } async provisionTenantResources(tenant) { const resources = { database: `${this.config.isolation.databasePrefix}${tenant.slug}`, storage: `storage_${tenant.slug}`, }; if (this.config.isolation.strategy === 'schema') { resources.schema = `${this.config.isolation.schemaPrefix}${tenant.slug}`; } this.logger.log(`Provisioned resources for tenant ${tenant.slug}:`, resources); return resources; } async updateTenant(tenantId, updates) { const tenant = this.tenants.get(tenantId); if (!tenant) return null; const updatedTenant = { ...tenant, ...updates, updatedAt: new Date(), }; this.tenants.set(tenantId, updatedTenant); this.tenantSubject.next(updatedTenant); this.logger.log(`Tenant updated: ${tenantId}`); return updatedTenant; } async suspendTenant(tenantId, reason) { const tenant = this.tenants.get(tenantId); if (!tenant) return false; tenant.status = 'suspended'; tenant.updatedAt = new Date(); if (reason) { tenant.metadata.suspensionReason = reason; } this.tenants.set(tenantId, tenant); this.tenantSubject.next(tenant); this.logger.log(`Tenant suspended: ${tenantId}${reason ? ` - ${reason}` : ''}`); return true; } async activateTenant(tenantId) { const tenant = this.tenants.get(tenantId); if (!tenant) return false; tenant.status = 'active'; tenant.updatedAt = new Date(); delete tenant.metadata.suspensionReason; this.tenants.set(tenantId, tenant); this.tenantSubject.next(tenant); this.logger.log(`Tenant activated: ${tenantId}`); return true; } async deleteTenant(tenantId) { const tenant = this.tenants.get(tenantId); if (!tenant) return false; tenant.status = 'deleted'; tenant.updatedAt = new Date(); this.tenants.set(tenantId, tenant); this.tenantSubject.next(tenant); setTimeout(async () => { await this.hardDeleteTenant(tenantId); }, 30 * 24 * 60 * 60 * 1000); this.logger.log(`Tenant marked for deletion: ${tenantId}`); return true; } async hardDeleteTenant(tenantId) { const tenant = this.tenants.get(tenantId); if (!tenant || tenant.status !== 'deleted') return; await this.deleteTenantResources(tenant); this.tenants.delete(tenantId); this.tenantMetrics.delete(tenantId); this.quotaExceeded.delete(tenantId); this.logger.log(`Tenant permanently deleted: ${tenantId}`); } async deleteTenantResources(tenant) { this.logger.log(`Deleting resources for tenant: ${tenant.slug}`); } async monitorTenantResources() { for (const tenant of this.tenants.values()) { if (tenant.status !== 'active') continue; await this.checkTenantQuotas(tenant); await this.updateTenantUsage(tenant); } } async checkTenantQuotas(tenant) { const { usage, limits } = tenant; if (usage.storage > limits.storage) { await this.handleQuotaExceeded(tenant.id, 'storage', usage.storage, limits.storage); } if (usage.requests > limits.requests) { await this.handleQuotaExceeded(tenant.id, 'requests', usage.requests, limits.requests); } if (usage.users > limits.users) { await this.handleQuotaExceeded(tenant.id, 'users', usage.users, limits.users); } if (usage.watchers > limits.watchers) { await this.handleQuotaExceeded(tenant.id, 'watchers', usage.watchers, limits.watchers); } if (usage.apiCalls > limits.apiCalls) { await this.handleQuotaExceeded(tenant.id, 'apiCalls', usage.apiCalls, limits.apiCalls); } } async handleQuotaExceeded(tenantId, resource, current, limit) { const quotaEvent = { tenantId, resource, current, limit, timestamp: new Date(), }; if (!this.quotaExceeded.has(tenantId)) { this.quotaExceeded.set(tenantId, []); } this.quotaExceeded.get(tenantId).push(quotaEvent); this.quotaSubject.next(quotaEvent); this.logger.warn(`Quota exceeded for tenant ${tenantId}: ${resource} (${current}/${limit})`); const exceededQuotas = this.quotaExceeded.get(tenantId) || []; const recentExceeded = exceededQuotas.filter((q) => Date.now() - q.timestamp.getTime() < 24 * 60 * 60 * 1000); if (recentExceeded.length >= 3) { await this.suspendTenant(tenantId, 'Multiple quota violations'); } } async updateTenantUsage(tenant) { const usage = { storage: Math.random() * tenant.limits.storage * 0.8, requests: Math.floor(Math.random() * tenant.limits.requests * 0.1), users: Math.min(tenant.usage.users, tenant.limits.users), watchers: Math.min(tenant.usage.watchers, tenant.limits.watchers), apiCalls: Math.floor(Math.random() * tenant.limits.apiCalls * 0.1), customFields: Math.min(tenant.usage.customFields, tenant.limits.customFields), lastUpdated: new Date(), }; tenant.usage = usage; this.tenants.set(tenant.id, tenant); const metrics = { tenantId: tenant.id, timestamp: new Date(), requests: usage.requests, storage: usage.storage, users: usage.users, errors: 0, performance: { averageResponseTime: Math.random() * 100 + 50, throughput: usage.requests / 24, errorRate: Math.random() * 0.05, }, }; if (!this.tenantMetrics.has(tenant.id)) { this.tenantMetrics.set(tenant.id, []); } this.tenantMetrics.get(tenant.id).push(metrics); this.metricsSubject.next(metrics); const recentMetrics = this.tenantMetrics.get(tenant.id).slice(-1000); this.tenantMetrics.set(tenant.id, recentMetrics); } async getTenantContext(tenantId) { const tenant = this.tenants.get(tenantId); if (!tenant || tenant.status !== 'active') return null; const isolation = { database: `${this.config.isolation.databasePrefix}${tenant.slug}`, prefix: `${tenant.slug}_`, }; if (this.config.isolation.strategy === 'schema') { isolation.schema = `${this.config.isolation.schemaPrefix}${tenant.slug}`; } return { tenant, isolation }; } async validateTenantAccess(tenantId, userId) { const tenant = this.tenants.get(tenantId); if (!tenant || tenant.status !== 'active') return false; return true; } async updateTenantBranding(tenantId, branding) { const tenant = this.tenants.get(tenantId); if (!tenant) return null; tenant.branding = { ...tenant.branding, ...branding }; tenant.updatedAt = new Date(); this.tenants.set(tenantId, tenant); this.tenantSubject.next(tenant); this.logger.log(`Branding updated for tenant: ${tenantId}`); return tenant; } async updateTenantConfiguration(tenantId, configuration) { const tenant = this.tenants.get(tenantId); if (!tenant) return null; tenant.configuration = { ...tenant.configuration, ...configuration }; tenant.updatedAt = new Date(); this.tenants.set(tenantId, tenant); this.tenantSubject.next(tenant); this.logger.log(`Configuration updated for tenant: ${tenantId}`); return tenant; } getTenants() { return Array.from(this.tenants.values()); } getTenantById(tenantId) { return this.tenants.get(tenantId); } getTenantBySlug(slug) { return Array.from(this.tenants.values()).find((t) => t.slug === slug); } getTenantMetrics(tenantId) { return this.tenantMetrics.get(tenantId) || []; } getQuotaExceeded(tenantId) { return this.quotaExceeded.get(tenantId) || []; } getTenantUpdates() { return this.tenantSubject.asObservable(); } getMetricsUpdates() { return this.metricsSubject.asObservable(); } getQuotaUpdates() { return this.quotaSubject.asObservable(); } async getTenantReport(tenantId) { const tenant = this.tenants.get(tenantId); if (!tenant) return null; return { tenant, metrics: this.getTenantMetrics(tenantId), quotaExceeded: this.getQuotaExceeded(tenantId), usage: tenant.usage, }; } async shutdown() { if (this.monitoringInterval) { clearInterval(this.monitoringInterval); } this.logger.log('Multi-tenant service shutdown'); } }; exports.MultiTenantService = MultiTenantService; exports.MultiTenantService = MultiTenantService = MultiTenantService_1 = __decorate([ (0, common_1.Injectable)(), __param(0, (0, common_2.Inject)('TELESCOPE_CONFIG')), __metadata("design:paramtypes", [Object]) ], MultiTenantService); //# sourceMappingURL=multi-tenant.service.js.map