diff --git a/.env.example b/.env.example index 02e7f87b..92e83303 100644 --- a/.env.example +++ b/.env.example @@ -26,3 +26,45 @@ EMAIL_FROM_NAME=TeachLink # Application URL (for tracking links) APP_URL=http://localhost:3000 + +# ============================================================================= +# Feature Flags - Module Loading Configuration +# ============================================================================= +# Set to 'false' to disable loading of specific modules at startup +# This reduces memory footprint and improves startup time +# ============================================================================= + +# Core Features (recommended to keep enabled) +ENABLE_AUTH=true +ENABLE_SESSION_MANAGEMENT=true + +# Optional Feature Modules +ENABLE_PAYMENTS=true +ENABLE_AB_TESTING=false +ENABLE_DATA_WAREHOUSE=false +ENABLE_COLLABORATION=true +ENABLE_MEDIA_PROCESSING=true +ENABLE_BACKUP=true +ENABLE_GRAPHQL=false +ENABLE_SYNC=true +ENABLE_MIGRATIONS=true +ENABLE_RATE_LIMITING=true +ENABLE_OBSERVABILITY=true +ENABLE_CACHING=true +ENABLE_FEATURE_FLAGS=true +ENABLE_SEARCH=true +ENABLE_NOTIFICATIONS=true +ENABLE_EMAIL_MARKETING=true +ENABLE_GAMIFICATION=true +ENABLE_ASSESSMENT=true +ENABLE_LEARNING_PATHS=true +ENABLE_MODERATION=true +ENABLE_ORCHESTRATION=true +ENABLE_SECURITY=true +ENABLE_TENANCY=true +ENABLE_CDN=true + +# Performance Tuning +# Set to 'true' to enable cluster mode (uses all CPU cores) +CLUSTER_MODE=false +CLUSTER_WORKERS=4 diff --git a/.husky/pre-commit b/.husky/pre-commit index 1e5dbac0..19bfd7ec 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,20 +1,3 @@ -#!/usr/bin/env sh -# .husky/pre-commit -# -# Runs automatically before every `git commit`. -# Exits non-zero to abort the commit if any check fails. -# -# What runs: -# lint-staged — Prettier (format) + ESLint (lint) on staged files only. -# -# To skip this hook in an emergency (NOT recommended for normal flow): -# git commit --no-verify -m "your message" -# -# To re-install hooks after a fresh `npm ci`: -# npm run prepare - -. "$(dirname -- "$0")/_/husky.sh" - echo "⚡ Running pre-commit checks via lint-staged..." npx lint-staged --config lint-staged.config.js diff --git a/src/app.module.ts b/src/app.module.ts index a78f600d..fde1bf3d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,15 +1,28 @@ -import { Module } from '@nestjs/common'; +import { Module, DynamicModule } from '@nestjs/common'; import { APP_INTERCEPTOR, APP_GUARD } from '@nestjs/core'; import { ConfigModule } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { redisStore } from 'cache-manager-redis-store'; - import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MonitoringModule } from './monitoring/monitoring.module'; import { MonitoringInterceptor } from './common/interceptors/monitoring.interceptor'; import { TypeOrmMonitoringLogger } from './monitoring/logging/typeorm-logger'; import { MetricsCollectionService } from './monitoring/metrics/metrics-collection.service'; +import { DatabaseModule } from './common/database/database.module'; +import { BullModule } from '@nestjs/bull'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { CacheModule } from '@nestjs/cache-manager'; +import { envValidationSchema } from './config/env.validation'; +import { HealthModule } from './health/health.module'; +import { SessionModule } from './session/session.module'; +import { createBullRedisClient } from './common/utils/bull-redis.util'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { CustomThrottleGuard } from './common/guards/throttle.guard'; +import { loadFeatureFlags } from './config/feature-flags.config'; +import { StartupLogger } from './common/lazy-loading/startup-logger.service'; + +// Feature modules - conditionally loaded based on feature flags import { SyncModule } from './sync/sync.module'; import { MediaModule } from './media/media.module'; import { BackupModule } from './backup/backup.module'; @@ -20,88 +33,325 @@ import { GraphQLModule } from './graphql/graphql.module'; import { MigrationModule } from './migrations/migration.module'; import { ABTestingModule } from './ab-testing/ab-testing.module'; import { ObservabilityModule } from './observability/observability.module'; -import { DatabaseModule } from './common/database/database.module'; -import { BullModule } from '@nestjs/bull'; -import { EventEmitterModule } from '@nestjs/event-emitter'; -import { CacheModule } from '@nestjs/cache-manager'; import { RateLimitingModule } from './rate-limiting/services/rate-limiting.module'; -import { envValidationSchema } from './config/env.validation'; -import { HealthModule } from './health/health.module'; -import { SessionModule } from './session/session.module'; -import { createBullRedisClient } from './common/utils/bull-redis.util'; -import { ThrottlerModule } from '@nestjs/throttler'; -import { CustomThrottleGuard } from './common/guards/throttle.guard'; +import { CachingModule } from './caching/caching.module'; +import { FeatureFlagsModule } from './feature-flags/feature-flags.module'; +import { SearchModule } from './search/search.module'; +import { NotificationsModule } from './notifications/notifications.module'; +import { EmailMarketingModule } from './email-marketing/email-marketing.module'; +import { GamificationModule } from './gamification/gamification.module'; +import { AssessmentModule } from './assessment/assessment.module'; +import { LearningPathsModule } from './learning-paths/learning-paths.module'; +import { ModerationModule } from './moderation/moderation.module'; +import { OrchestrationModule } from './orchestration/orchestration.module'; +import { SecurityModule } from './security/security.module'; +import { TenancyModule } from './tenancy/tenancy.module'; +import { CDNModule } from './cdn/cdn.module'; +import { AuthModule } from './auth/auth.module'; +import { PaymentsModule } from './payments/payments.module'; + +@Module({}) +export class AppModule { + static async forRoot(): Promise { + const flags = loadFeatureFlags(); + const startupLogger = new StartupLogger(); -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - validationSchema: envValidationSchema, - }), - TypeOrmModule.forRootAsync({ - imports: [MonitoringModule], - inject: [MetricsCollectionService], - useFactory: (metricsService: MetricsCollectionService) => ({ - type: 'postgres', - host: process.env.DATABASE_HOST || 'localhost', - port: parseInt(process.env.DATABASE_PORT || '5432'), - username: process.env.DATABASE_USER || 'postgres', - password: process.env.DATABASE_PASSWORD || 'postgres', - database: process.env.DATABASE_NAME || 'teachlink', - autoLoadEntities: true, - synchronize: process.env.NODE_ENV !== 'production', - logging: true, - logger: new TypeOrmMonitoringLogger(metricsService), - maxQueryExecutionTime: 1000, + // Core modules - always loaded + const coreModules = [ + ConfigModule.forRoot({ + isGlobal: true, + validationSchema: envValidationSchema, + }), + TypeOrmModule.forRootAsync({ + imports: [MonitoringModule], + inject: [MetricsCollectionService], + useFactory: (metricsService: MetricsCollectionService) => ({ + type: 'postgres', + host: process.env.DATABASE_HOST || 'localhost', + port: parseInt(process.env.DATABASE_PORT || '5432'), + username: process.env.DATABASE_USER || 'postgres', + password: process.env.DATABASE_PASSWORD || 'postgres', + database: process.env.DATABASE_NAME || 'teachlink', + autoLoadEntities: true, + synchronize: process.env.NODE_ENV !== 'production', + logging: true, + logger: new TypeOrmMonitoringLogger(metricsService), + maxQueryExecutionTime: 1000, + }), }), - }), - MonitoringModule, - EventEmitterModule.forRoot(), - BullModule.forRoot({ - redis: { + MonitoringModule, + EventEmitterModule.forRoot(), + BullModule.forRoot({ + redis: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379', 10), + }, + createClient: createBullRedisClient, + }), + CacheModule.register({ + isGlobal: true, + store: redisStore, host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379', 10), - }, - createClient: createBullRedisClient, - }), - CacheModule.register({ - isGlobal: true, - store: redisStore, - host: process.env.REDIS_HOST || 'localhost', - port: parseInt(process.env.REDIS_PORT || '6379'), - }), - SessionModule, - ThrottlerModule.forRoot([ - { - ttl: parseInt(process.env.THROTTLE_TTL || '60'), - limit: parseInt(process.env.THROTTLE_LIMIT || '10'), - }, - ]), - HealthModule, - SyncModule, - MediaModule, - BackupModule, - CollaborationModule, - DataWarehouseModule, - QueueModule, - GraphQLModule, - MigrationModule, - ABTestingModule, - ObservabilityModule, - RateLimitingModule, - DatabaseModule, - ], - controllers: [AppController], - providers: [ - AppService, - { - provide: APP_INTERCEPTOR, - useClass: MonitoringInterceptor, - }, - { - provide: APP_GUARD, - useClass: CustomThrottleGuard, - }, - ], -}) -export class AppModule {} + port: parseInt(process.env.REDIS_PORT || '6379'), + }), + SessionModule, + ThrottlerModule.forRoot([ + { + ttl: parseInt(process.env.THROTTLE_TTL || '60'), + limit: parseInt(process.env.THROTTLE_LIMIT || '10'), + }, + ]), + HealthModule, + DatabaseModule, + ]; + + // Feature modules - conditionally loaded based on feature flags + const featureModules: DynamicModule[] = []; + + // Auth Module + if (flags.ENABLE_AUTH) { + const startTime = Date.now(); + featureModules.push(AuthModule); + startupLogger.recordModuleLoaded('AuthModule', startTime); + } else { + startupLogger.recordModuleSkipped('AuthModule', 'ENABLE_AUTH=false'); + } + + // Payments Module + if (flags.ENABLE_PAYMENTS) { + const startTime = Date.now(); + featureModules.push(PaymentsModule); + startupLogger.recordModuleLoaded('PaymentsModule', startTime); + } else { + startupLogger.recordModuleSkipped('PaymentsModule', 'ENABLE_PAYMENTS=false'); + } + + // AB Testing Module + if (flags.ENABLE_AB_TESTING) { + const startTime = Date.now(); + featureModules.push(ABTestingModule); + startupLogger.recordModuleLoaded('ABTestingModule', startTime); + } else { + startupLogger.recordModuleSkipped('ABTestingModule', 'ENABLE_AB_TESTING=false'); + } + + // Data Warehouse Module + if (flags.ENABLE_DATA_WAREHOUSE) { + const startTime = Date.now(); + featureModules.push(DataWarehouseModule); + startupLogger.recordModuleLoaded('DataWarehouseModule', startTime); + } else { + startupLogger.recordModuleSkipped('DataWarehouseModule', 'ENABLE_DATA_WAREHOUSE=false'); + } + + // Collaboration Module + if (flags.ENABLE_COLLABORATION) { + const startTime = Date.now(); + featureModules.push(CollaborationModule); + startupLogger.recordModuleLoaded('CollaborationModule', startTime); + } else { + startupLogger.recordModuleSkipped('CollaborationModule', 'ENABLE_COLLABORATION=false'); + } + + // Media Module + if (flags.ENABLE_MEDIA_PROCESSING) { + const startTime = Date.now(); + featureModules.push(MediaModule); + startupLogger.recordModuleLoaded('MediaModule', startTime); + } else { + startupLogger.recordModuleSkipped('MediaModule', 'ENABLE_MEDIA_PROCESSING=false'); + } + + // Backup Module + if (flags.ENABLE_BACKUP) { + const startTime = Date.now(); + featureModules.push(BackupModule); + startupLogger.recordModuleLoaded('BackupModule', startTime); + } else { + startupLogger.recordModuleSkipped('BackupModule', 'ENABLE_BACKUP=false'); + } + + // GraphQL Module + if (flags.ENABLE_GRAPHQL) { + const startTime = Date.now(); + featureModules.push(GraphQLModule); + startupLogger.recordModuleLoaded('GraphQLModule', startTime); + } else { + startupLogger.recordModuleSkipped('GraphQLModule', 'ENABLE_GRAPHQL=false'); + } + + // Sync Module + if (flags.ENABLE_SYNC) { + const startTime = Date.now(); + featureModules.push(SyncModule); + startupLogger.recordModuleLoaded('SyncModule', startTime); + } else { + startupLogger.recordModuleSkipped('SyncModule', 'ENABLE_SYNC=false'); + } + + // Migration Module + if (flags.ENABLE_MIGRATIONS) { + const startTime = Date.now(); + featureModules.push(MigrationModule); + startupLogger.recordModuleLoaded('MigrationModule', startTime); + } else { + startupLogger.recordModuleSkipped('MigrationModule', 'ENABLE_MIGRATIONS=false'); + } + + // Rate Limiting Module + if (flags.ENABLE_RATE_LIMITING) { + const startTime = Date.now(); + featureModules.push(RateLimitingModule); + startupLogger.recordModuleLoaded('RateLimitingModule', startTime); + } else { + startupLogger.recordModuleSkipped('RateLimitingModule', 'ENABLE_RATE_LIMITING=false'); + } + + // Observability Module + if (flags.ENABLE_OBSERVABILITY) { + const startTime = Date.now(); + featureModules.push(ObservabilityModule); + startupLogger.recordModuleLoaded('ObservabilityModule', startTime); + } else { + startupLogger.recordModuleSkipped('ObservabilityModule', 'ENABLE_OBSERVABILITY=false'); + } + + // Caching Module + if (flags.ENABLE_CACHING) { + const startTime = Date.now(); + featureModules.push(CachingModule); + startupLogger.recordModuleLoaded('CachingModule', startTime); + } else { + startupLogger.recordModuleSkipped('CachingModule', 'ENABLE_CACHING=false'); + } + + // Feature Flags Module + if (flags.ENABLE_FEATURE_FLAGS) { + const startTime = Date.now(); + featureModules.push(FeatureFlagsModule); + startupLogger.recordModuleLoaded('FeatureFlagsModule', startTime); + } else { + startupLogger.recordModuleSkipped('FeatureFlagsModule', 'ENABLE_FEATURE_FLAGS=false'); + } + + // Search Module + if (flags.ENABLE_SEARCH) { + const startTime = Date.now(); + featureModules.push(SearchModule); + startupLogger.recordModuleLoaded('SearchModule', startTime); + } else { + startupLogger.recordModuleSkipped('SearchModule', 'ENABLE_SEARCH=false'); + } + + // Notifications Module + if (flags.ENABLE_NOTIFICATIONS) { + const startTime = Date.now(); + featureModules.push(NotificationsModule); + startupLogger.recordModuleLoaded('NotificationsModule', startTime); + } else { + startupLogger.recordModuleSkipped('NotificationsModule', 'ENABLE_NOTIFICATIONS=false'); + } + + // Email Marketing Module + if (flags.ENABLE_EMAIL_MARKETING) { + const startTime = Date.now(); + featureModules.push(EmailMarketingModule); + startupLogger.recordModuleLoaded('EmailMarketingModule', startTime); + } else { + startupLogger.recordModuleSkipped('EmailMarketingModule', 'ENABLE_EMAIL_MARKETING=false'); + } + + // Gamification Module + if (flags.ENABLE_GAMIFICATION) { + const startTime = Date.now(); + featureModules.push(GamificationModule); + startupLogger.recordModuleLoaded('GamificationModule', startTime); + } else { + startupLogger.recordModuleSkipped('GamificationModule', 'ENABLE_GAMIFICATION=false'); + } + + // Assessment Module + if (flags.ENABLE_ASSESSMENT) { + const startTime = Date.now(); + featureModules.push(AssessmentModule); + startupLogger.recordModuleLoaded('AssessmentModule', startTime); + } else { + startupLogger.recordModuleSkipped('AssessmentModule', 'ENABLE_ASSESSMENT=false'); + } + + // Learning Paths Module + if (flags.ENABLE_LEARNING_PATHS) { + const startTime = Date.now(); + featureModules.push(LearningPathsModule); + startupLogger.recordModuleLoaded('LearningPathsModule', startTime); + } else { + startupLogger.recordModuleSkipped('LearningPathsModule', 'ENABLE_LEARNING_PATHS=false'); + } + + // Moderation Module + if (flags.ENABLE_MODERATION) { + const startTime = Date.now(); + featureModules.push(ModerationModule); + startupLogger.recordModuleLoaded('ModerationModule', startTime); + } else { + startupLogger.recordModuleSkipped('ModerationModule', 'ENABLE_MODERATION=false'); + } + + // Orchestration Module + if (flags.ENABLE_ORCHESTRATION) { + const startTime = Date.now(); + featureModules.push(OrchestrationModule); + startupLogger.recordModuleLoaded('OrchestrationModule', startTime); + } else { + startupLogger.recordModuleSkipped('OrchestrationModule', 'ENABLE_ORCHESTRATION=false'); + } + + // Security Module + if (flags.ENABLE_SECURITY) { + const startTime = Date.now(); + featureModules.push(SecurityModule); + startupLogger.recordModuleLoaded('SecurityModule', startTime); + } else { + startupLogger.recordModuleSkipped('SecurityModule', 'ENABLE_SECURITY=false'); + } + + // Tenancy Module + if (flags.ENABLE_TENANCY) { + const startTime = Date.now(); + featureModules.push(TenancyModule); + startupLogger.recordModuleLoaded('TenancyModule', startTime); + } else { + startupLogger.recordModuleSkipped('TenancyModule', 'ENABLE_TENANCY=false'); + } + + // CDN Module + if (flags.ENABLE_CDN) { + const startTime = Date.now(); + featureModules.push(CDNModule); + startupLogger.recordModuleLoaded('CDNModule', startTime); + } else { + startupLogger.recordModuleSkipped('CDNModule', 'ENABLE_CDN=false'); + } + + // Queue Module (always loaded for Bull) + featureModules.push(QueueModule); + + return { + module: AppModule, + imports: [...coreModules, ...featureModules], + controllers: [AppController], + providers: [ + AppService, + StartupLogger, + { + provide: APP_INTERCEPTOR, + useClass: MonitoringInterceptor, + }, + { + provide: APP_GUARD, + useClass: CustomThrottleGuard, + }, + ], + }; + } +} diff --git a/src/common/lazy-loading/index.ts b/src/common/lazy-loading/index.ts new file mode 100644 index 00000000..03b3b621 --- /dev/null +++ b/src/common/lazy-loading/index.ts @@ -0,0 +1,3 @@ +export * from './lazy-module-loader.service'; +export * from './lazy-loading.module'; +export * from './startup-logger.service'; diff --git a/src/common/lazy-loading/lazy-loading.module.ts b/src/common/lazy-loading/lazy-loading.module.ts new file mode 100644 index 00000000..adc7aaa3 --- /dev/null +++ b/src/common/lazy-loading/lazy-loading.module.ts @@ -0,0 +1,18 @@ +import { Module, Global, DynamicModule } from '@nestjs/common'; +import { LazyModuleLoader } from './lazy-module-loader.service'; + +@Global() +@Module({ + providers: [LazyModuleLoader], + exports: [LazyModuleLoader], +}) +export class LazyLoadingModule { + static forRoot(): DynamicModule { + return { + module: LazyLoadingModule, + providers: [LazyModuleLoader], + exports: [LazyModuleLoader], + global: true, + }; + } +} diff --git a/src/common/lazy-loading/lazy-module-loader.service.ts b/src/common/lazy-loading/lazy-module-loader.service.ts new file mode 100644 index 00000000..9c0501c8 --- /dev/null +++ b/src/common/lazy-loading/lazy-module-loader.service.ts @@ -0,0 +1,222 @@ +import { Injectable, Logger, DynamicModule } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; + +export interface LazyModuleOptions { + moduleName: string; + featureFlag?: string; + dependencies?: string[]; +} + +export interface ModuleLoadResult { + moduleName: string; + loadedAt: Date; + loadTimeMs: number; + success: boolean; + error?: Error; +} + +@Injectable() +export class LazyModuleLoader { + private readonly logger = new Logger(LazyModuleLoader.name); + private loadedModules = new Map>(); + private moduleLoadResults = new Map(); + private moduleRegistry = new Map Promise>(); + + constructor(private readonly moduleRef: ModuleRef) {} + + /** + * Register a lazy-loadable module + */ + register(moduleName: string, factory: () => Promise): void { + if (this.moduleRegistry.has(moduleName)) { + this.logger.warn(`Module ${moduleName} is already registered`); + return; + } + this.moduleRegistry.set(moduleName, factory); + this.logger.debug(`Registered lazy module: ${moduleName}`); + } + + /** + * Load a module on-demand + */ + async load(moduleName: string): Promise { + // Check if already loaded + if (this.loadedModules.has(moduleName)) { + this.logger.debug(`Module ${moduleName} is already loaded`); + const loadedModule = this.loadedModules.get(moduleName); + if (loadedModule) { + return loadedModule; + } + } + + // Check if registered + const factory = this.moduleRegistry.get(moduleName); + if (!factory) { + this.logger.error(`Module ${moduleName} is not registered for lazy loading`); + return null; + } + + // Load the module + const startTime = Date.now(); + this.logger.log(`Loading module: ${moduleName}...`); + + const loadPromise = this.loadModuleInternal(moduleName, factory, startTime); + this.loadedModules.set(moduleName, loadPromise); + + return loadPromise; + } + + /** + * Load multiple modules + */ + async loadMany(moduleNames: string[]): Promise { + const results = await Promise.all(moduleNames.map((name) => this.load(name))); + return results.filter((m): m is DynamicModule => m !== null); + } + + /** + * Check if a module is loaded + */ + isLoaded(moduleName: string): boolean { + return this.loadedModules.has(moduleName); + } + + /** + * Check if a module is registered + */ + isRegistered(moduleName: string): boolean { + return this.moduleRegistry.has(moduleName); + } + + /** + * Get list of loaded module names + */ + getLoadedModules(): string[] { + return Array.from(this.loadedModules.keys()); + } + + /** + * Get list of registered module names + */ + getRegisteredModules(): string[] { + return Array.from(this.moduleRegistry.keys()); + } + + /** + * Get load result for a module + */ + getLoadResult(moduleName: string): ModuleLoadResult | undefined { + return this.moduleLoadResults.get(moduleName); + } + + /** + * Get all load results + */ + getAllLoadResults(): ModuleLoadResult[] { + return Array.from(this.moduleLoadResults.values()); + } + + /** + * Get total load time for all loaded modules + */ + getTotalLoadTime(): number { + let total = 0; + for (const result of this.moduleLoadResults.values()) { + if (result.success) { + total += result.loadTimeMs; + } + } + return total; + } + + /** + * Unload a module (for testing/memory management) + */ + async unload(moduleName: string): Promise { + if (!this.loadedModules.has(moduleName)) { + return false; + } + + this.loadedModules.delete(moduleName); + this.moduleLoadResults.delete(moduleName); + this.logger.log(`Unloaded module: ${moduleName}`); + return true; + } + + /** + * Preload modules that are likely to be needed + */ + async preload(moduleNames: string[]): Promise { + this.logger.log(`Preloading ${moduleNames.length} modules...`); + const startTime = Date.now(); + + await this.loadMany(moduleNames); + + const duration = Date.now() - startTime; + this.logger.log(`Preloaded ${moduleNames.length} modules in ${duration}ms`); + } + + /** + * Generate a report of all module loading activity + */ + generateReport(): { + registered: number; + loaded: number; + totalLoadTime: number; + averageLoadTime: number; + modules: ModuleLoadResult[]; + } { + const results = this.getAllLoadResults(); + const successfulLoads = results.filter((r) => r.success); + const totalLoadTime = this.getTotalLoadTime(); + + return { + registered: this.moduleRegistry.size, + loaded: this.loadedModules.size, + totalLoadTime, + averageLoadTime: successfulLoads.length > 0 ? totalLoadTime / successfulLoads.length : 0, + modules: results, + }; + } + + private async loadModuleInternal( + moduleName: string, + factory: () => Promise, + startTime: number, + ): Promise { + try { + const module = await factory(); + const loadTimeMs = Date.now() - startTime; + + const result: ModuleLoadResult = { + moduleName, + loadedAt: new Date(), + loadTimeMs, + success: true, + }; + + this.moduleLoadResults.set(moduleName, result); + this.logger.log(`Module ${moduleName} loaded in ${loadTimeMs}ms`); + + return module; + } catch (error) { + const loadTimeMs = Date.now() - startTime; + + const result: ModuleLoadResult = { + moduleName, + loadedAt: new Date(), + loadTimeMs, + success: false, + error: error as Error, + }; + + this.moduleLoadResults.set(moduleName, result); + this.logger.error( + `Failed to load module ${moduleName} after ${loadTimeMs}ms`, + (error as Error).message, + ); + + throw error; + } + } +} diff --git a/src/common/lazy-loading/startup-logger.service.ts b/src/common/lazy-loading/startup-logger.service.ts new file mode 100644 index 00000000..d59690f3 --- /dev/null +++ b/src/common/lazy-loading/startup-logger.service.ts @@ -0,0 +1,192 @@ +import { Injectable, Logger, OnModuleInit, OnApplicationBootstrap } from '@nestjs/common'; + +export interface StartupMetrics { + bootstrapStartTime: number; + bootstrapEndTime: number; + totalStartupTimeMs: number; + moduleInitTimeMs: number; + modulesLoaded: string[]; + modulesSkipped: string[]; + memoryUsage: NodeJS.MemoryUsage; +} + +export interface ModuleLoadMetric { + moduleName: string; + startTime: number; + endTime: number; + durationMs: number; + dependencies: string[]; +} + +@Injectable() +export class StartupLogger implements OnModuleInit, OnApplicationBootstrap { + private readonly logger = new Logger(StartupLogger.name); + private bootstrapStartTime: number = 0; + private moduleInitStartTime: number = 0; + private moduleMetrics: Map = new Map(); + private modulesLoaded: string[] = []; + private modulesSkipped: string[] = []; + + constructor() { + this.bootstrapStartTime = Date.now(); + this.moduleInitStartTime = this.bootstrapStartTime; + } + + onModuleInit() { + this.moduleInitStartTime = Date.now(); + this.logger.log('Module initialization started'); + } + + onApplicationBootstrap() { + const metrics = this.generateMetrics(); + this.logStartupReport(metrics); + } + + /** + * Record a module being loaded + */ + recordModuleLoaded(moduleName: string, startTime: number, dependencies: string[] = []): void { + const endTime = Date.now(); + const durationMs = endTime - startTime; + + this.moduleMetrics.set(moduleName, { + moduleName, + startTime, + endTime, + durationMs, + dependencies, + }); + + this.modulesLoaded.push(moduleName); + this.logger.debug(`Module ${moduleName} loaded in ${durationMs}ms`); + } + + /** + * Record a module being skipped (feature flag disabled) + */ + recordModuleSkipped(moduleName: string, reason: string): void { + this.modulesSkipped.push(moduleName); + this.logger.debug(`Module ${moduleName} skipped: ${reason}`); + } + + /** + * Get all recorded metrics + */ + getMetrics(): StartupMetrics { + return this.generateMetrics(); + } + + /** + * Get module load metrics + */ + getModuleMetrics(): ModuleLoadMetric[] { + return Array.from(this.moduleMetrics.values()); + } + + /** + * Get slowest loading modules + */ + getSlowestModules(limit: number = 5): ModuleLoadMetric[] { + return this.getModuleMetrics() + .sort((a, b) => b.durationMs - a.durationMs) + .slice(0, limit); + } + + /** + * Get total startup time + */ + getTotalStartupTime(): number { + return Date.now() - this.bootstrapStartTime; + } + + /** + * Generate a complete startup report + */ + private generateMetrics(): StartupMetrics { + const now = Date.now(); + + return { + bootstrapStartTime: this.bootstrapStartTime, + bootstrapEndTime: now, + totalStartupTimeMs: now - this.bootstrapStartTime, + moduleInitTimeMs: now - this.moduleInitStartTime, + modulesLoaded: this.modulesLoaded, + modulesSkipped: this.modulesSkipped, + memoryUsage: process.memoryUsage(), + }; + } + + /** + * Log startup report + */ + private logStartupReport(metrics: StartupMetrics): void { + const totalModules = metrics.modulesLoaded.length + metrics.modulesSkipped.length; + const loadedCount = metrics.modulesLoaded.length; + const skippedCount = metrics.modulesSkipped.length; + + this.logger.log('========================================'); + this.logger.log(' STARTUP REPORT'); + this.logger.log('========================================'); + this.logger.log(`Total Startup Time: ${metrics.totalStartupTimeMs}ms`); + this.logger.log(`Module Init Time: ${metrics.moduleInitTimeMs}ms`); + this.logger.log(`Modules Loaded: ${loadedCount}/${totalModules}`); + this.logger.log(`Modules Skipped: ${skippedCount}/${totalModules}`); + + if (skippedCount > 0) { + this.logger.log(`Skipped Modules: ${metrics.modulesSkipped.join(', ')}`); + } + + // Memory usage + const memoryMB = Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024); + this.logger.log(`Memory Usage: ${memoryMB}MB`); + + // Slowest modules + const slowest = this.getSlowestModules(3); + if (slowest.length > 0) { + this.logger.log('Slowest Modules:'); + slowest.forEach((m) => { + this.logger.log(` - ${m.moduleName}: ${m.durationMs}ms`); + }); + } + + this.logger.log('========================================'); + + // Performance warning if startup is slow + if (metrics.totalStartupTimeMs > 10000) { + this.logger.warn('Startup time exceeds 10 seconds - consider optimizing module loading'); + } + } + + /** + * Generate JSON report for external monitoring + */ + generateJSONReport(): Record { + const metrics = this.generateMetrics(); + + return { + timestamp: new Date().toISOString(), + startup: { + totalTimeMs: metrics.totalStartupTimeMs, + moduleInitTimeMs: metrics.moduleInitTimeMs, + }, + modules: { + total: metrics.modulesLoaded.length + metrics.modulesSkipped.length, + loaded: metrics.modulesLoaded.length, + skipped: metrics.modulesSkipped.length, + loadedList: metrics.modulesLoaded, + skippedList: metrics.modulesSkipped, + }, + memory: { + heapUsed: Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024), + heapTotal: Math.round(metrics.memoryUsage.heapTotal / 1024 / 1024), + rss: Math.round(metrics.memoryUsage.rss / 1024 / 1024), + external: Math.round(metrics.memoryUsage.external / 1024 / 1024), + }, + moduleMetrics: this.getModuleMetrics().map((m) => ({ + name: m.moduleName, + loadTimeMs: m.durationMs, + dependencies: m.dependencies, + })), + }; + } +} diff --git a/src/config/feature-flags.config.ts b/src/config/feature-flags.config.ts new file mode 100644 index 00000000..b420d3ab --- /dev/null +++ b/src/config/feature-flags.config.ts @@ -0,0 +1,197 @@ +/** + * Feature flags configuration for conditional module loading + * Used to control which modules are loaded at application startup + */ + +export interface FeatureFlagsConfig { + // Core feature flags + ENABLE_AUTH: boolean; + ENABLE_SESSION_MANAGEMENT: boolean; + + // Optional feature modules + ENABLE_PAYMENTS: boolean; + ENABLE_AB_TESTING: boolean; + ENABLE_DATA_WAREHOUSE: boolean; + ENABLE_COLLABORATION: boolean; + ENABLE_MEDIA_PROCESSING: boolean; + ENABLE_BACKUP: boolean; + ENABLE_GRAPHQL: boolean; + ENABLE_SYNC: boolean; + ENABLE_MIGRATIONS: boolean; + ENABLE_RATE_LIMITING: boolean; + ENABLE_OBSERVABILITY: boolean; + ENABLE_CACHING: boolean; + ENABLE_FEATURE_FLAGS: boolean; + ENABLE_SEARCH: boolean; + ENABLE_NOTIFICATIONS: boolean; + ENABLE_EMAIL_MARKETING: boolean; + ENABLE_GAMIFICATION: boolean; + ENABLE_ASSESSMENT: boolean; + ENABLE_LEARNING_PATHS: boolean; + ENABLE_MODERATION: boolean; + ENABLE_ORCHESTRATION: boolean; + ENABLE_SECURITY: boolean; + ENABLE_TENANCY: boolean; + ENABLE_CDN: boolean; +} + +/** + * Default feature flags - all features enabled by default + */ +export const defaultFeatureFlags: FeatureFlagsConfig = { + ENABLE_AUTH: true, + ENABLE_SESSION_MANAGEMENT: true, + ENABLE_PAYMENTS: true, + ENABLE_AB_TESTING: false, + ENABLE_DATA_WAREHOUSE: false, + ENABLE_COLLABORATION: true, + ENABLE_MEDIA_PROCESSING: true, + ENABLE_BACKUP: true, + ENABLE_GRAPHQL: false, + ENABLE_SYNC: true, + ENABLE_MIGRATIONS: true, + ENABLE_RATE_LIMITING: true, + ENABLE_OBSERVABILITY: true, + ENABLE_CACHING: true, + ENABLE_FEATURE_FLAGS: true, + ENABLE_SEARCH: true, + ENABLE_NOTIFICATIONS: true, + ENABLE_EMAIL_MARKETING: true, + ENABLE_GAMIFICATION: true, + ENABLE_ASSESSMENT: true, + ENABLE_LEARNING_PATHS: true, + ENABLE_MODERATION: true, + ENABLE_ORCHESTRATION: true, + ENABLE_SECURITY: true, + ENABLE_TENANCY: true, + ENABLE_CDN: true, +}; + +/** + * Load feature flags from environment variables + */ +export function loadFeatureFlags(): FeatureFlagsConfig { + return { + ENABLE_AUTH: getBooleanEnv('ENABLE_AUTH', defaultFeatureFlags.ENABLE_AUTH), + ENABLE_SESSION_MANAGEMENT: getBooleanEnv( + 'ENABLE_SESSION_MANAGEMENT', + defaultFeatureFlags.ENABLE_SESSION_MANAGEMENT, + ), + ENABLE_PAYMENTS: getBooleanEnv('ENABLE_PAYMENTS', defaultFeatureFlags.ENABLE_PAYMENTS), + ENABLE_AB_TESTING: getBooleanEnv('ENABLE_AB_TESTING', defaultFeatureFlags.ENABLE_AB_TESTING), + ENABLE_DATA_WAREHOUSE: getBooleanEnv( + 'ENABLE_DATA_WAREHOUSE', + defaultFeatureFlags.ENABLE_DATA_WAREHOUSE, + ), + ENABLE_COLLABORATION: getBooleanEnv( + 'ENABLE_COLLABORATION', + defaultFeatureFlags.ENABLE_COLLABORATION, + ), + ENABLE_MEDIA_PROCESSING: getBooleanEnv( + 'ENABLE_MEDIA_PROCESSING', + defaultFeatureFlags.ENABLE_MEDIA_PROCESSING, + ), + ENABLE_BACKUP: getBooleanEnv('ENABLE_BACKUP', defaultFeatureFlags.ENABLE_BACKUP), + ENABLE_GRAPHQL: getBooleanEnv('ENABLE_GRAPHQL', defaultFeatureFlags.ENABLE_GRAPHQL), + ENABLE_SYNC: getBooleanEnv('ENABLE_SYNC', defaultFeatureFlags.ENABLE_SYNC), + ENABLE_MIGRATIONS: getBooleanEnv('ENABLE_MIGRATIONS', defaultFeatureFlags.ENABLE_MIGRATIONS), + ENABLE_RATE_LIMITING: getBooleanEnv( + 'ENABLE_RATE_LIMITING', + defaultFeatureFlags.ENABLE_RATE_LIMITING, + ), + ENABLE_OBSERVABILITY: getBooleanEnv( + 'ENABLE_OBSERVABILITY', + defaultFeatureFlags.ENABLE_OBSERVABILITY, + ), + ENABLE_CACHING: getBooleanEnv('ENABLE_CACHING', defaultFeatureFlags.ENABLE_CACHING), + ENABLE_FEATURE_FLAGS: getBooleanEnv( + 'ENABLE_FEATURE_FLAGS', + defaultFeatureFlags.ENABLE_FEATURE_FLAGS, + ), + ENABLE_SEARCH: getBooleanEnv('ENABLE_SEARCH', defaultFeatureFlags.ENABLE_SEARCH), + ENABLE_NOTIFICATIONS: getBooleanEnv( + 'ENABLE_NOTIFICATIONS', + defaultFeatureFlags.ENABLE_NOTIFICATIONS, + ), + ENABLE_EMAIL_MARKETING: getBooleanEnv( + 'ENABLE_EMAIL_MARKETING', + defaultFeatureFlags.ENABLE_EMAIL_MARKETING, + ), + ENABLE_GAMIFICATION: getBooleanEnv( + 'ENABLE_GAMIFICATION', + defaultFeatureFlags.ENABLE_GAMIFICATION, + ), + ENABLE_ASSESSMENT: getBooleanEnv('ENABLE_ASSESSMENT', defaultFeatureFlags.ENABLE_ASSESSMENT), + ENABLE_LEARNING_PATHS: getBooleanEnv( + 'ENABLE_LEARNING_PATHS', + defaultFeatureFlags.ENABLE_LEARNING_PATHS, + ), + ENABLE_MODERATION: getBooleanEnv('ENABLE_MODERATION', defaultFeatureFlags.ENABLE_MODERATION), + ENABLE_ORCHESTRATION: getBooleanEnv( + 'ENABLE_ORCHESTRATION', + defaultFeatureFlags.ENABLE_ORCHESTRATION, + ), + ENABLE_SECURITY: getBooleanEnv('ENABLE_SECURITY', defaultFeatureFlags.ENABLE_SECURITY), + ENABLE_TENANCY: getBooleanEnv('ENABLE_TENANCY', defaultFeatureFlags.ENABLE_TENANCY), + ENABLE_CDN: getBooleanEnv('ENABLE_CDN', defaultFeatureFlags.ENABLE_CDN), + }; +} + +/** + * Helper function to parse boolean environment variables + */ +function getBooleanEnv(key: string, defaultValue: boolean): boolean { + const value = process.env[key]; + if (value === undefined) { + return defaultValue; + } + return value.toLowerCase() === 'true' || value === '1'; +} + +/** + * Get list of enabled modules based on feature flags + */ +export function getEnabledModules(flags: FeatureFlagsConfig): string[] { + const modules: string[] = []; + + if (flags.ENABLE_AUTH) modules.push('AuthModule'); + if (flags.ENABLE_PAYMENTS) modules.push('PaymentsModule'); + if (flags.ENABLE_AB_TESTING) modules.push('ABTestingModule'); + if (flags.ENABLE_DATA_WAREHOUSE) modules.push('DataWarehouseModule'); + if (flags.ENABLE_COLLABORATION) modules.push('CollaborationModule'); + if (flags.ENABLE_MEDIA_PROCESSING) modules.push('MediaModule'); + if (flags.ENABLE_BACKUP) modules.push('BackupModule'); + if (flags.ENABLE_GRAPHQL) modules.push('GraphQLModule'); + if (flags.ENABLE_SYNC) modules.push('SyncModule'); + if (flags.ENABLE_MIGRATIONS) modules.push('MigrationModule'); + if (flags.ENABLE_RATE_LIMITING) modules.push('RateLimitingModule'); + if (flags.ENABLE_OBSERVABILITY) modules.push('ObservabilityModule'); + if (flags.ENABLE_CACHING) modules.push('CachingModule'); + if (flags.ENABLE_FEATURE_FLAGS) modules.push('FeatureFlagsModule'); + if (flags.ENABLE_SEARCH) modules.push('SearchModule'); + if (flags.ENABLE_NOTIFICATIONS) modules.push('NotificationsModule'); + if (flags.ENABLE_EMAIL_MARKETING) modules.push('EmailMarketingModule'); + if (flags.ENABLE_GAMIFICATION) modules.push('GamificationModule'); + if (flags.ENABLE_ASSESSMENT) modules.push('AssessmentModule'); + if (flags.ENABLE_LEARNING_PATHS) modules.push('LearningPathsModule'); + if (flags.ENABLE_MODERATION) modules.push('ModerationModule'); + if (flags.ENABLE_ORCHESTRATION) modules.push('OrchestrationModule'); + if (flags.ENABLE_SECURITY) modules.push('SecurityModule'); + if (flags.ENABLE_TENANCY) modules.push('TenancyModule'); + if (flags.ENABLE_CDN) modules.push('CDNModule'); + + return modules; +} + +/** + * Get list of disabled modules based on feature flags + */ +export function getDisabledModules(flags: FeatureFlagsConfig): string[] { + const allModules = Object.keys(defaultFeatureFlags) + .filter((key) => key.startsWith('ENABLE_')) + .map((key) => `${key.replace('ENABLE_', '')}Module`); + + const enabledModules = getEnabledModules(flags); + + return allModules.filter((module) => !enabledModules.includes(module)); +} diff --git a/src/main.ts b/src/main.ts index 828bb943..2ff06f3d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,10 @@ import { SESSION_REDIS_CLIENT } from './session/session.constants'; async function bootstrapWorker() { const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); + const bootstrapStartTime = Date.now(); + + // Create the application with dynamic module loading + const app = await NestFactory.create(await AppModule.forRoot()); const redisClient = app.get(SESSION_REDIS_CLIENT); @@ -87,6 +90,8 @@ async function bootstrapWorker() { const port = process.env.PORT || 3000; await app.listen(port); + const startupTime = Date.now() - bootstrapStartTime; + if (sessionConfig.stickySessionsRequired) { logger.log( 'Sticky sessions are enabled by policy. Configure LB cookie affinity on teachlink.sid.', @@ -95,6 +100,7 @@ async function bootstrapWorker() { logger.log(`🚀 TeachLink API running on http://localhost:${port}`); logger.log(`📚 Swagger docs available at http://localhost:${port}/api`); + logger.log(`⏱️ Application startup completed in ${startupTime}ms`); } async function bootstrap() {