UNPKG

@sehirapp/core-microservice

Version:

Modern mikroservis core paketi - MongoDB 6.7, Express API, Mongoose, PM2 cluster desteği

946 lines (839 loc) 33.4 kB
import express from 'express'; import { logger } from './logger.js'; import dbManager from './database.js'; import middleware from './middleware.js'; import BillingClient from './BillingClient.js'; import { billingCallback } from './middleware/billing.js'; import dotenv from 'dotenv'; // Environment variables yükle dotenv.config(); /** * Modern MicroService Class - Express API server ile entegre * Database, middleware, routing ve lifecycle yönetimi sağlar */ export default class MicroService { constructor(options = {}) { const { name, version = '1.0.0', port = process.env.PORT || 3000, host = process.env.HOST || '0.0.0.0', // Express options trustProxy = process.env.TRUST_PROXY === 'true', // Middleware options enableApiKeyAuth = process.env.ENABLE_API_KEY_AUTH !== 'false', enableRateLimit = process.env.ENABLE_RATE_LIMIT !== 'false', enableCors = process.env.ENABLE_CORS !== 'false', enableSecurity = process.env.ENABLE_SECURITY !== 'false', enableRequestLogging = process.env.ENABLE_REQUEST_LOGGING !== 'false', enableErrorHandling = process.env.ENABLE_ERROR_HANDLING !== 'false', // Database options useMongoose = process.env.USE_MONGOOSE !== 'false', autoConnectDb = process.env.AUTO_CONNECT_DB !== 'false', // Billing options enableBilling = process.env.ENABLE_BILLING !== 'false', billingUrl = process.env.BILLING_URL, billingApiKey = process.env.BILLING_API_KEY, billingWebhookSecret = process.env.BILLING_WEBHOOK_SECRET, billingCallbackHandler = null, // Custom options routes = [], middlewares = [], beforeStart = null, afterStart = null, beforeStop = null, afterStop = null } = options; // Temel özellikler this.name = name; this.version = version; this.port = parseInt(port); this.host = host; // Express app this.app = express(); this.server = null; this.isRunning = false; this.isInitialized = false; // Options this.options = { trustProxy, enableApiKeyAuth, enableRateLimit, enableCors, enableSecurity, enableRequestLogging, enableErrorHandling, useMongoose, autoConnectDb, enableBilling, billingUrl, billingApiKey, billingWebhookSecret }; // Custom konfigurasyon this.routes = routes; this.middlewares = middlewares; // Lifecycle hooks this.beforeStart = beforeStart; this.afterStart = afterStart; this.beforeStop = beforeStop; this.afterStop = afterStop; // Billing callback handler this.billingCallbackHandler = billingCallbackHandler; // Database manager referansı this.dbManager = dbManager; // Billing client this.billingClient = null; if (this.options.enableBilling) { this.billingClient = new BillingClient({ billingUrl: this.options.billingUrl, serviceType: this.name, headers: this.options.billingApiKey ? { 'X-API-Key': this.options.billingApiKey } : {} }); } // Logger'a service name set et if (logger && typeof logger.serviceName !== 'undefined') { logger.serviceName = this.name; } logger.info(`Initializing ${this.name} v${this.version}`, { serviceName: this.name, version: this.version, port: this.port, host: this.host, options: this.options }); this.setupExpress(); // Built-in error handlers ve graceful shutdown'u otomatik kur this.setupGracefulShutdown(); } /** * Express app'i konfigüre et */ setupExpress() { // Trust proxy ayarı if (this.options.trustProxy) { this.app.set('trust proxy', true); } // JSON pretty print (development) if (process.env.NODE_ENV === 'development') { this.app.set('json spaces', 2); } // Disable x-powered-by header this.app.disable('x-powered-by'); logger.debug('Express app configured', { serviceName: this.name, trustProxy: this.options.trustProxy }); } /** * Middleware'leri kur */ setupMiddleware() { logger.info('Setting up middleware...', this.options); // Core middleware'leri kur middleware.setupMiddleware(this.app, { apiKey: this.options.enableApiKeyAuth, rateLimit: this.options.enableRateLimit, cors: this.options.enableCors, security: this.options.enableSecurity, requestLogging: this.options.enableRequestLogging, errorHandling: this.options.enableErrorHandling }); // Custom middleware'leri ekle if (this.middlewares && this.middlewares.length > 0) { this.middlewares.forEach((customMiddleware, index) => { if (typeof customMiddleware === 'function') { this.app.use(customMiddleware); logger.debug(`Custom middleware ${index + 1} added`); } }); } logger.info('Middleware setup completed'); } /** * Route'ları kur */ setupRoutes() { logger.info('Setting up routes...'); // Health check endpoint this.app.get('/health', middleware.healthCheck(this.dbManager)); this.app.get('/status', this.getStatusEndpoint.bind(this)); // API info endpoint this.app.get('/api/info', (req, res) => { res.json({ name: this.name, version: this.version, environment: process.env.NODE_ENV || 'development', uptime: process.uptime(), timestamp: Date.now() }); }); // Billing callback endpoint (otomatik) if (this.options.enableBilling && this.billingCallbackHandler) { // this.app.post('/api/billingCallback', billingCallback(this.billingCallbackHandler)); // get callbackUrl from env const callbackEndpoint = process.env.BILLING_CALLBACK_ENDPOINT || `/api/billingCallback`; this.app.post(callbackEndpoint, billingCallback(this.billingCallbackHandler)); // get callbackUrl from env logger.info('Built-in billing callback endpoint registered: POST ${callbackEndpoint}'); } // Custom route'ları ekle if (this.routes && this.routes.length > 0) { this.routes.forEach((route, index) => { if (route.path && route.handler) { const method = route.method || 'get'; const middlewares = route.middlewares || []; // Route'u app'e ekle if (typeof this.app[method] === 'function') { this.app[method](route.path, ...middlewares, route.handler); logger.debug(`Custom route added: ${method.toUpperCase()} ${route.path}`); } else { logger.warn(`Invalid HTTP method: ${method} for route ${route.path}`); } } else { logger.warn(`Invalid route configuration at index ${index}`); } }); } // 404 handler (en sonda) if (this.options.enableErrorHandling) { this.app.use(middleware.notFoundHandler()); this.app.use(middleware.errorHandler()); } logger.info('Routes setup completed'); } /** * Status endpoint handler */ async getStatusEndpoint(req, res, next) { try { const status = await this.getStatus(); res.json(status); } catch (error) { next(error); } } /** * Database bağlantısını kur */ async connectDb() { try { logger.info(`${this.name}: Connecting to database...`); if (this.options.useMongoose) { await this.dbManager.connectMongoose(); logger.info(`${this.name}: Mongoose connection established`); } else { await this.dbManager.connect(); logger.info(`${this.name}: MongoDB native connection established`); } return true; } catch (err) { logger.error(`${this.name}: Database connection failed`, { error: err.message, serviceName: this.name, useMongoose: this.options.useMongoose }); throw err; } } /** * Database bağlantısını kapat */ async disconnectDb() { try { logger.info(`${this.name}: Closing database connection...`); await this.dbManager.closeConnection(`${this.name} shutdown`); logger.info(`${this.name}: Database connection closed`); } catch (err) { logger.error(`${this.name}: Error closing database connection`, { error: err.message, serviceName: this.name }); } } /** * Servis başlatma - Express server ile */ async start() { const startTime = Date.now(); try { logger.info(`🚀 Starting ${this.name} v${this.version}...`, { serviceName: this.name, version: this.version, port: this.port, host: this.host, nodeVersion: process.version, environment: process.env.NODE_ENV || 'development', timestamp: Date.now() }); // Before start hook if (this.beforeStart && typeof this.beforeStart === 'function') { const hookStartTime = Date.now(); logger.info('⚙️ Running beforeStart hook...', { serviceName: this.name, hookName: 'beforeStart' }); try { await this.beforeStart(this); const hookDuration = Date.now() - hookStartTime; logger.info('✅ beforeStart hook completed', { serviceName: this.name, hookName: 'beforeStart', duration: `${hookDuration}ms` }); } catch (error) { const hookDuration = Date.now() - hookStartTime; logger.error('❌ beforeStart hook failed', { serviceName: this.name, hookName: 'beforeStart', duration: `${hookDuration}ms`, error: error.message, stack: error.stack }); throw new Error(`beforeStart hook failed: ${error.message}`); } } // Database bağlantısını kur if (this.options.autoConnectDb) { const dbStartTime = Date.now(); logger.info('🗄️ Initializing database connection...', { serviceName: this.name, useMongoose: this.options.useMongoose }); await this.connectDb(); // Database health check const health = await this.dbManager.healthCheck(); if (health.status !== 'connected') { throw new Error(`Database health check failed: ${health.error || health.status}`); } const dbDuration = Date.now() - dbStartTime; logger.info('✅ Database connection established', { serviceName: this.name, dbStatus: health.status, connectionType: health.connectionType, pingTime: health.pingTime, database: health.database, duration: `${dbDuration}ms` }); } // Middleware'leri kur const middlewareStartTime = Date.now(); logger.info('🔧 Setting up middleware...', { serviceName: this.name, middlewareOptions: this.options }); this.setupMiddleware(); const middlewareDuration = Date.now() - middlewareStartTime; logger.info('✅ Middleware setup completed', { serviceName: this.name, duration: `${middlewareDuration}ms` }); // Route'ları kur const routeStartTime = Date.now(); logger.info('🛤️ Setting up routes...', { serviceName: this.name, customRoutesCount: this.routes.length }); this.setupRoutes(); const routeDuration = Date.now() - routeStartTime; logger.info('✅ Routes setup completed', { serviceName: this.name, customRoutesCount: this.routes.length, duration: `${routeDuration}ms` }); // Server'ı başlat const serverStartTime = Date.now(); logger.info('🌐 Starting Express server...', { serviceName: this.name, port: this.port, host: this.host }); await this.startServer(); const serverDuration = Date.now() - serverStartTime; logger.info('✅ Express server started', { serviceName: this.name, port: this.port, host: this.host, duration: `${serverDuration}ms` }); // Billing client callback URL'sini set et ve app.locals'a kaydet if (this.billingClient && this.billingCallbackHandler) { // const callbackUrl = `http://${this.host}:${this.port}/api/billingCallback`; // get callbackUrl from env const callbackUrl = process.env.BILLING_CALLBACK_URL || `http://${this.host}:${this.port}/api/billingCallback`; this.billingClient.setCallbackUrl(callbackUrl); logger.info('🔗 Billing callback URL configured', { callbackUrl }); } // Controller'larda erişim için app.locals'a kaydet this.app.locals.billingClient = this.billingClient; this.app.locals.microService = this; this.app.locals.dbManager = this.dbManager; logger.info('📎 Services registered to app.locals for controller access', { billingClient: !!this.billingClient, dbManager: !!this.dbManager }); this.isInitialized = true; // After start hook if (this.afterStart && typeof this.afterStart === 'function') { const hookStartTime = Date.now(); logger.info('⚙️ Running afterStart hook...', { serviceName: this.name, hookName: 'afterStart' }); try { await this.afterStart(this); const hookDuration = Date.now() - hookStartTime; logger.info('✅ afterStart hook completed', { serviceName: this.name, hookName: 'afterStart', duration: `${hookDuration}ms` }); } catch (error) { const hookDuration = Date.now() - hookStartTime; logger.warn('⚠️ afterStart hook failed (non-blocking)', { serviceName: this.name, hookName: 'afterStart', duration: `${hookDuration}ms`, error: error.message, stack: error.stack }); // afterStart hataları non-blocking olsun } } const totalDuration = Date.now() - startTime; logger.info(`🎉 ${this.name} v${this.version} started successfully`, { serviceName: this.name, version: this.version, port: this.port, host: this.host, pid: process.pid, nodeVersion: process.version, environment: process.env.NODE_ENV || 'development', totalStartupTime: `${totalDuration}ms`, endpoints: { health: `http://${this.host}:${this.port}/health`, status: `http://${this.host}:${this.port}/status`, api: `http://${this.host}:${this.port}/api` }, timestamp: Date.now() }); return true; } catch (error) { const totalDuration = Date.now() - startTime; logger.error(`💥 Failed to start ${this.name}`, { error: error.message, stack: error.stack, serviceName: this.name, version: this.version, totalFailureTime: `${totalDuration}ms`, timestamp: Date.now() }); // Cleanup on failure try { await this.cleanup('startup-failure'); } catch (cleanupError) { logger.error('Error during startup cleanup', { error: cleanupError.message, originalError: error.message }); } throw error; } } /** * Express server'ı başlat */ async startServer() { return new Promise((resolve, reject) => { this.server = this.app.listen(this.port, this.host, (err) => { if (err) { reject(err); } else { this.isRunning = true; logger.info(`Express server listening on ${this.host}:${this.port}`, { serviceName: this.name, port: this.port, host: this.host }); resolve(); } }); // Server error handling this.server.on('error', (error) => { if (error.code === 'EADDRINUSE') { logger.error(`Port ${this.port} is already in use`, { port: this.port, serviceName: this.name }); } else { logger.error('Server error', { error: error.message, serviceName: this.name }); } reject(error); }); }); } /** * Servis durdurma */ async stop() { const stopTime = Date.now(); try { logger.info(`🛑 Stopping ${this.name}...`, { serviceName: this.name, version: this.version, reason: 'manual-stop', timestamp: Date.now() }); // Before stop hook if (this.beforeStop && typeof this.beforeStop === 'function') { const hookStartTime = Date.now(); logger.info('⚙️ Running beforeStop hook...', { serviceName: this.name, hookName: 'beforeStop' }); try { await this.beforeStop(this); const hookDuration = Date.now() - hookStartTime; logger.info('✅ beforeStop hook completed', { serviceName: this.name, hookName: 'beforeStop', duration: `${hookDuration}ms` }); } catch (error) { const hookDuration = Date.now() - hookStartTime; logger.warn('⚠️ beforeStop hook failed (non-blocking)', { serviceName: this.name, hookName: 'beforeStop', duration: `${hookDuration}ms`, error: error.message, stack: error.stack }); // beforeStop hataları non-blocking olsun } } // Server'ı durdur if (this.server && this.isRunning) { const serverStopTime = Date.now(); logger.info('🌐 Stopping Express server...', { serviceName: this.name, port: this.port }); await this.stopServer(); const serverStopDuration = Date.now() - serverStopTime; logger.info('✅ Express server stopped', { serviceName: this.name, port: this.port, duration: `${serverStopDuration}ms` }); } // Database bağlantısını kapat if (this.options.autoConnectDb) { const dbStopTime = Date.now(); logger.info('🗄️ Closing database connections...', { serviceName: this.name }); await this.disconnectDb(); const dbStopDuration = Date.now() - dbStopTime; logger.info('✅ Database connections closed', { serviceName: this.name, duration: `${dbStopDuration}ms` }); } this.isInitialized = false; // After stop hook if (this.afterStop && typeof this.afterStop === 'function') { const hookStartTime = Date.now(); logger.info('⚙️ Running afterStop hook...', { serviceName: this.name, hookName: 'afterStop' }); try { await this.afterStop(this); const hookDuration = Date.now() - hookStartTime; logger.info('✅ afterStop hook completed', { serviceName: this.name, hookName: 'afterStop', duration: `${hookDuration}ms` }); } catch (error) { const hookDuration = Date.now() - hookStartTime; logger.warn('⚠️ afterStop hook failed (non-blocking)', { serviceName: this.name, hookName: 'afterStop', duration: `${hookDuration}ms`, error: error.message, stack: error.stack }); // afterStop hataları non-blocking olsun } } const totalDuration = Date.now() - stopTime; logger.info(`✅ ${this.name} stopped successfully`, { serviceName: this.name, version: this.version, totalStopTime: `${totalDuration}ms`, timestamp: Date.now() }); return true; } catch (error) { const totalDuration = Date.now() - stopTime; logger.error(`💥 Error stopping ${this.name}`, { error: error.message, stack: error.stack, serviceName: this.name, totalFailureTime: `${totalDuration}ms`, timestamp: Date.now() }); throw error; } } /** * Express server'ı durdur */ async stopServer() { return new Promise((resolve, reject) => { this.server.close((err) => { if (err) { logger.error('Error closing server', { error: err.message }); reject(err); } else { this.isRunning = false; logger.info('Express server closed', { serviceName: this.name }); resolve(); } }); }); } /** * Servis durumunu kontrol et */ async getStatus() { try { const dbHealth = this.options.autoConnectDb ? await this.dbManager.healthCheck() : null; const billingHealth = this.billingClient ? await this.billingClient.healthCheck() : null; return { serviceName: this.name, version: this.version, port: this.port, host: this.host, isInitialized: this.isInitialized, isRunning: this.isRunning, database: dbHealth, billing: billingHealth, uptime: process.uptime(), memory: process.memoryUsage(), environment: process.env.NODE_ENV || 'development', pid: process.pid, nodeVersion: process.version, options: this.options, timestamp: Date.now() }; } catch (error) { logger.error(`Error getting status for ${this.name}`, { error: error.message, serviceName: this.name }); return { serviceName: this.name, version: this.version, port: this.port, host: this.host, isInitialized: this.isInitialized, isRunning: this.isRunning, error: error.message, timestamp: Date.now() }; } } /** * Route ekle */ addRoute(method, path, handler, middlewares = []) { if (!this.isRunning) { if (typeof this.app[method] === 'function') { this.app[method](path, ...middlewares, handler); logger.debug(`Route added: ${method.toUpperCase()} ${path}`); } else { throw new Error(`Invalid HTTP method: ${method}`); } } else { logger.warn('Cannot add routes after server has started'); } } /** * Middleware ekle */ addMiddleware(middleware) { if (!this.isRunning) { this.app.use(middleware); logger.debug('Middleware added'); } else { logger.warn('Cannot add middleware after server has started'); } } /** * Express app'e erişim */ getApp() { return this.app; } /** * Database manager'a erişim */ getDbManager() { return this.dbManager; } /** * Logger'a erişim */ getLogger() { return logger; } /** * Billing client'a erişim */ getBillingClient() { return this.billingClient; } /** * Server instance'a erişim */ getServer() { return this.server; } /** * Cleanup işlemleri */ async cleanup(reason = 'unknown') { logger.info(`🧹 Starting cleanup process...`, { serviceName: this.name, reason, timestamp: Date.now() }); const cleanupPromises = []; // Server'ı durdur if (this.server && this.isRunning) { cleanupPromises.push( this.stopServer().catch(error => { logger.warn('Error stopping server during cleanup', { error: error.message }); }) ); } // Database bağlantısını kapat if (this.options.autoConnectDb) { cleanupPromises.push( this.disconnectDb().catch(error => { logger.warn('Error disconnecting database during cleanup', { error: error.message }); }) ); } // Tüm cleanup işlemlerini bekle await Promise.allSettled(cleanupPromises); logger.info(`✅ Cleanup process completed`, { serviceName: this.name, reason }); } /** * Built-in Error Handlers - Graceful shutdown setup */ setupGracefulShutdown() { const gracefulShutdown = async (signal, error = null) => { logger.warn(`🚨 ${signal} received, starting graceful shutdown...`, { signal, serviceName: this.name, version: this.version, pid: process.pid, uptime: process.uptime(), memoryUsage: process.memoryUsage(), error: error?.message || null, timestamp: Date.now() }); try { // Eğer servis çalışıyorsa normal shutdown yap if (this.isInitialized) { await this.stop(); } else { // Başlatma sırasında hata varsa cleanup yap await this.cleanup(`graceful-shutdown-${signal.toLowerCase()}`); } logger.info(`✅ Graceful shutdown completed successfully`, { signal, serviceName: this.name, timestamp: Date.now() }); process.exit(0); } catch (shutdownError) { logger.error(`💥 Error during graceful shutdown`, { signal, serviceName: this.name, shutdownError: shutdownError.message, originalError: error?.message || null, stack: shutdownError.stack, timestamp: Date.now() }); // Force exit after 5 seconds setTimeout(() => { logger.error('🚨 Force exiting after failed graceful shutdown'); process.exit(1); }, 5000); } }; // Signal handlers process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGHUP', () => gracefulShutdown('SIGHUP')); // Built-in Error Handlers process.on('uncaughtException', (error) => { logger.error('💥 Uncaught Exception - Critical Error', { serviceName: this.name, error: error.message, stack: error.stack, type: 'uncaughtException', pid: process.pid, memoryUsage: process.memoryUsage(), timestamp: Date.now() }); gracefulShutdown('UNCAUGHT_EXCEPTION', error); }); process.on('unhandledRejection', (reason, promise) => { const error = reason instanceof Error ? reason : new Error(String(reason)); logger.error('💥 Unhandled Promise Rejection - Critical Error', { serviceName: this.name, error: error.message, stack: error.stack, reason: String(reason), promise: promise.toString(), type: 'unhandledRejection', pid: process.pid, memoryUsage: process.memoryUsage(), timestamp: Date.now() }); gracefulShutdown('UNHANDLED_REJECTION', error); }); // Memory usage monitoring process.on('warning', (warning) => { logger.warn('⚠️ Node.js Warning', { serviceName: this.name, warningName: warning.name, warningMessage: warning.message, stack: warning.stack, timestamp: Date.now() }); }); logger.info('🛡️ Built-in error handlers and graceful shutdown registered', { serviceName: this.name, handlers: [ 'SIGINT', 'SIGTERM', 'SIGHUP', 'uncaughtException', 'unhandledRejection', 'warning' ], pid: process.pid, timestamp: Date.now() }); } }