@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
JavaScript
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()
});
}
}