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