|
|
const express = require('express') |
|
|
const cors = require('cors') |
|
|
const helmet = require('helmet') |
|
|
const compression = require('compression') |
|
|
const path = require('path') |
|
|
const fs = require('fs') |
|
|
const bcrypt = require('bcryptjs') |
|
|
|
|
|
const config = require('../config/config') |
|
|
const logger = require('./utils/logger') |
|
|
const redis = require('./models/redis') |
|
|
const pricingService = require('./services/pricingService') |
|
|
const cacheMonitor = require('./utils/cacheMonitor') |
|
|
|
|
|
|
|
|
const apiRoutes = require('./routes/api') |
|
|
const unifiedRoutes = require('./routes/unified') |
|
|
const adminRoutes = require('./routes/admin') |
|
|
const webRoutes = require('./routes/web') |
|
|
const apiStatsRoutes = require('./routes/apiStats') |
|
|
const geminiRoutes = require('./routes/geminiRoutes') |
|
|
const openaiGeminiRoutes = require('./routes/openaiGeminiRoutes') |
|
|
const standardGeminiRoutes = require('./routes/standardGeminiRoutes') |
|
|
const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes') |
|
|
const openaiRoutes = require('./routes/openaiRoutes') |
|
|
const droidRoutes = require('./routes/droidRoutes') |
|
|
const userRoutes = require('./routes/userRoutes') |
|
|
const azureOpenaiRoutes = require('./routes/azureOpenaiRoutes') |
|
|
const webhookRoutes = require('./routes/webhook') |
|
|
|
|
|
|
|
|
const { |
|
|
corsMiddleware, |
|
|
requestLogger, |
|
|
securityMiddleware, |
|
|
errorHandler, |
|
|
globalRateLimit, |
|
|
requestSizeLimit |
|
|
} = require('./middleware/auth') |
|
|
const { browserFallbackMiddleware } = require('./middleware/browserFallback') |
|
|
|
|
|
class Application { |
|
|
constructor() { |
|
|
this.app = express() |
|
|
this.server = null |
|
|
} |
|
|
|
|
|
async initialize() { |
|
|
try { |
|
|
|
|
|
logger.info('🔄 Connecting to Redis...') |
|
|
await redis.connect() |
|
|
logger.success('✅ Redis connected successfully') |
|
|
|
|
|
|
|
|
logger.info('🔄 Initializing pricing service...') |
|
|
await pricingService.initialize() |
|
|
|
|
|
|
|
|
logger.info('🔄 Initializing model service...') |
|
|
const modelService = require('./services/modelService') |
|
|
await modelService.initialize() |
|
|
|
|
|
|
|
|
await this.initializeCacheMonitoring() |
|
|
|
|
|
|
|
|
logger.info('🔄 Initializing admin credentials...') |
|
|
await this.initializeAdmin() |
|
|
|
|
|
|
|
|
logger.info('💰 Checking cost data initialization...') |
|
|
const costInitService = require('./services/costInitService') |
|
|
const needsInit = await costInitService.needsInitialization() |
|
|
if (needsInit) { |
|
|
logger.info('💰 Initializing cost data for all API Keys...') |
|
|
const result = await costInitService.initializeAllCosts() |
|
|
logger.info( |
|
|
`💰 Cost initialization completed: ${result.processed} processed, ${result.errors} errors` |
|
|
) |
|
|
} |
|
|
|
|
|
|
|
|
logger.info('🕐 Initializing Claude account session windows...') |
|
|
const claudeAccountService = require('./services/claudeAccountService') |
|
|
await claudeAccountService.initializeSessionWindows() |
|
|
|
|
|
|
|
|
this.app.use((req, res, next) => { |
|
|
if (req.path === '/admin-next/' && req.method === 'GET') { |
|
|
logger.warn('🚨 INTERCEPTING /admin-next/ request at the very beginning!') |
|
|
const adminSpaPath = path.join(__dirname, '..', 'web', 'admin-spa', 'dist') |
|
|
const indexPath = path.join(adminSpaPath, 'index.html') |
|
|
|
|
|
if (fs.existsSync(indexPath)) { |
|
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') |
|
|
return res.sendFile(indexPath) |
|
|
} else { |
|
|
logger.error('❌ index.html not found at:', indexPath) |
|
|
return res.status(404).send('index.html not found') |
|
|
} |
|
|
} |
|
|
next() |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.use( |
|
|
helmet({ |
|
|
contentSecurityPolicy: false, |
|
|
crossOriginEmbedderPolicy: false |
|
|
}) |
|
|
) |
|
|
|
|
|
|
|
|
if (config.web.enableCors) { |
|
|
this.app.use(cors()) |
|
|
} else { |
|
|
this.app.use(corsMiddleware) |
|
|
} |
|
|
|
|
|
|
|
|
this.app.use(browserFallbackMiddleware) |
|
|
|
|
|
|
|
|
this.app.use( |
|
|
compression({ |
|
|
filter: (req, res) => { |
|
|
|
|
|
if (res.getHeader('Content-Type') === 'text/event-stream') { |
|
|
return false |
|
|
} |
|
|
|
|
|
return compression.filter(req, res) |
|
|
} |
|
|
}) |
|
|
) |
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV === 'production') { |
|
|
this.app.use(globalRateLimit) |
|
|
} |
|
|
|
|
|
|
|
|
this.app.use(requestSizeLimit) |
|
|
|
|
|
|
|
|
this.app.use(requestLogger) |
|
|
|
|
|
|
|
|
if (process.env.DEBUG_HTTP_TRAFFIC === 'true') { |
|
|
try { |
|
|
const { debugInterceptor } = require('./middleware/debugInterceptor') |
|
|
this.app.use(debugInterceptor) |
|
|
logger.info('🐛 HTTP调试拦截器已启用 - 日志输出到 logs/http-debug-*.log') |
|
|
} catch (error) { |
|
|
logger.warn('⚠️ 无法加载HTTP调试拦截器:', error.message) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
this.app.use( |
|
|
express.json({ |
|
|
limit: '10mb', |
|
|
verify: (req, res, buf, encoding) => { |
|
|
|
|
|
if (buf && buf.length && !buf.toString(encoding || 'utf8').trim()) { |
|
|
throw new Error('Invalid JSON: empty body') |
|
|
} |
|
|
} |
|
|
}) |
|
|
) |
|
|
this.app.use(express.urlencoded({ extended: true, limit: '10mb' })) |
|
|
this.app.use(securityMiddleware) |
|
|
|
|
|
|
|
|
if (config.server.trustProxy) { |
|
|
this.app.set('trust proxy', 1) |
|
|
} |
|
|
|
|
|
|
|
|
this.app.use((req, res, next) => { |
|
|
if (req.path.startsWith('/admin-next')) { |
|
|
logger.info( |
|
|
`🔍 DEBUG: Incoming request - method: ${req.method}, path: ${req.path}, originalUrl: ${req.originalUrl}` |
|
|
) |
|
|
} |
|
|
next() |
|
|
}) |
|
|
|
|
|
|
|
|
const adminSpaPath = path.join(__dirname, '..', 'web', 'admin-spa', 'dist') |
|
|
if (fs.existsSync(adminSpaPath)) { |
|
|
|
|
|
this.app.get('/admin-next', (req, res) => { |
|
|
res.redirect(301, '/admin-next/') |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.all('/admin-next/', (req, res) => { |
|
|
logger.info('🎯 HIT: /admin-next/ route handler triggered!') |
|
|
logger.info(`Method: ${req.method}, Path: ${req.path}, URL: ${req.url}`) |
|
|
|
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') { |
|
|
return res.status(405).send('Method Not Allowed') |
|
|
} |
|
|
|
|
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') |
|
|
res.sendFile(path.join(adminSpaPath, 'index.html')) |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.get('/admin-next/*', (req, res) => { |
|
|
|
|
|
if (req.path === '/admin-next/') { |
|
|
logger.error('❌ ERROR: /admin-next/ should not reach here!') |
|
|
return res.status(500).send('Route configuration error') |
|
|
} |
|
|
|
|
|
const requestPath = req.path.replace('/admin-next/', '') |
|
|
|
|
|
|
|
|
if ( |
|
|
requestPath.includes('..') || |
|
|
requestPath.includes('//') || |
|
|
requestPath.includes('\\') |
|
|
) { |
|
|
return res.status(400).json({ error: 'Invalid path' }) |
|
|
} |
|
|
|
|
|
|
|
|
const filePath = path.join(adminSpaPath, requestPath) |
|
|
|
|
|
|
|
|
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { |
|
|
|
|
|
if (filePath.endsWith('.js') || filePath.endsWith('.css')) { |
|
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') |
|
|
} else if (filePath.endsWith('.html')) { |
|
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') |
|
|
} |
|
|
return res.sendFile(filePath) |
|
|
} |
|
|
|
|
|
|
|
|
if (requestPath.match(/\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf)$/i)) { |
|
|
return res.status(404).send('Not found') |
|
|
} |
|
|
|
|
|
|
|
|
res.sendFile(path.join(adminSpaPath, 'index.html')) |
|
|
}) |
|
|
|
|
|
logger.info('✅ Admin SPA (next) static files mounted at /admin-next/') |
|
|
} else { |
|
|
logger.warn('⚠️ Admin SPA dist directory not found, skipping /admin-next route') |
|
|
} |
|
|
|
|
|
|
|
|
this.app.use('/api', apiRoutes) |
|
|
this.app.use('/api', unifiedRoutes) |
|
|
this.app.use('/claude', apiRoutes) |
|
|
this.app.use('/admin', adminRoutes) |
|
|
this.app.use('/users', userRoutes) |
|
|
|
|
|
this.app.use('/web', webRoutes) |
|
|
this.app.use('/apiStats', apiStatsRoutes) |
|
|
|
|
|
this.app.use('/gemini', standardGeminiRoutes) |
|
|
this.app.use('/gemini', geminiRoutes) |
|
|
this.app.use('/openai/gemini', openaiGeminiRoutes) |
|
|
this.app.use('/openai/claude', openaiClaudeRoutes) |
|
|
this.app.use('/openai', unifiedRoutes) |
|
|
this.app.use('/openai', openaiRoutes) |
|
|
|
|
|
this.app.use('/droid', droidRoutes) |
|
|
this.app.use('/azure', azureOpenaiRoutes) |
|
|
this.app.use('/admin/webhook', webhookRoutes) |
|
|
|
|
|
|
|
|
this.app.get('/', (req, res) => { |
|
|
res.redirect('/admin-next/api-stats') |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.get('/health', async (req, res) => { |
|
|
try { |
|
|
const timer = logger.timer('health-check') |
|
|
|
|
|
|
|
|
const [redisHealth, loggerHealth] = await Promise.all([ |
|
|
this.checkRedisHealth(), |
|
|
this.checkLoggerHealth() |
|
|
]) |
|
|
|
|
|
const memory = process.memoryUsage() |
|
|
|
|
|
|
|
|
let version = process.env.APP_VERSION || process.env.VERSION |
|
|
if (!version) { |
|
|
try { |
|
|
const versionFile = path.join(__dirname, '..', 'VERSION') |
|
|
if (fs.existsSync(versionFile)) { |
|
|
version = fs.readFileSync(versionFile, 'utf8').trim() |
|
|
} |
|
|
} catch (error) { |
|
|
|
|
|
} |
|
|
} |
|
|
if (!version) { |
|
|
try { |
|
|
const { version: pkgVersion } = require('../package.json') |
|
|
version = pkgVersion |
|
|
} catch (error) { |
|
|
version = '1.0.0' |
|
|
} |
|
|
} |
|
|
|
|
|
const health = { |
|
|
status: 'healthy', |
|
|
service: 'claude-relay-service', |
|
|
version, |
|
|
timestamp: new Date().toISOString(), |
|
|
uptime: process.uptime(), |
|
|
memory: { |
|
|
used: `${Math.round(memory.heapUsed / 1024 / 1024)}MB`, |
|
|
total: `${Math.round(memory.heapTotal / 1024 / 1024)}MB`, |
|
|
external: `${Math.round(memory.external / 1024 / 1024)}MB` |
|
|
}, |
|
|
components: { |
|
|
redis: redisHealth, |
|
|
logger: loggerHealth |
|
|
}, |
|
|
stats: logger.getStats() |
|
|
} |
|
|
|
|
|
timer.end('completed') |
|
|
res.json(health) |
|
|
} catch (error) { |
|
|
logger.error('❌ Health check failed:', { error: error.message, stack: error.stack }) |
|
|
res.status(503).json({ |
|
|
status: 'unhealthy', |
|
|
error: error.message, |
|
|
timestamp: new Date().toISOString() |
|
|
}) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.get('/metrics', async (req, res) => { |
|
|
try { |
|
|
const stats = await redis.getSystemStats() |
|
|
const metrics = { |
|
|
...stats, |
|
|
uptime: process.uptime(), |
|
|
memory: process.memoryUsage(), |
|
|
timestamp: new Date().toISOString() |
|
|
} |
|
|
|
|
|
res.json(metrics) |
|
|
} catch (error) { |
|
|
logger.error('❌ Metrics collection failed:', error) |
|
|
res.status(500).json({ error: 'Failed to collect metrics' }) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.use('*', (req, res) => { |
|
|
res.status(404).json({ |
|
|
error: 'Not Found', |
|
|
message: `Route ${req.originalUrl} not found`, |
|
|
timestamp: new Date().toISOString() |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
this.app.use(errorHandler) |
|
|
|
|
|
logger.success('✅ Application initialized successfully') |
|
|
} catch (error) { |
|
|
logger.error('💥 Application initialization failed:', error) |
|
|
throw error |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async initializeAdmin() { |
|
|
try { |
|
|
const initFilePath = path.join(__dirname, '..', 'data', 'init.json') |
|
|
|
|
|
if (!fs.existsSync(initFilePath)) { |
|
|
logger.warn('⚠️ No admin credentials found. Please run npm run setup first.') |
|
|
return |
|
|
} |
|
|
|
|
|
|
|
|
const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8')) |
|
|
|
|
|
|
|
|
const saltRounds = 10 |
|
|
const passwordHash = await bcrypt.hash(initData.adminPassword, saltRounds) |
|
|
|
|
|
|
|
|
const adminCredentials = { |
|
|
username: initData.adminUsername, |
|
|
passwordHash, |
|
|
createdAt: initData.initializedAt || new Date().toISOString(), |
|
|
lastLogin: null, |
|
|
updatedAt: initData.updatedAt || null |
|
|
} |
|
|
|
|
|
await redis.setSession('admin_credentials', adminCredentials) |
|
|
|
|
|
logger.success('✅ Admin credentials loaded from init.json (single source of truth)') |
|
|
logger.info(`📋 Admin username: ${adminCredentials.username}`) |
|
|
} catch (error) { |
|
|
logger.error('❌ Failed to initialize admin credentials:', { |
|
|
error: error.message, |
|
|
stack: error.stack |
|
|
}) |
|
|
throw error |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async checkRedisHealth() { |
|
|
try { |
|
|
const start = Date.now() |
|
|
await redis.getClient().ping() |
|
|
const latency = Date.now() - start |
|
|
|
|
|
return { |
|
|
status: 'healthy', |
|
|
connected: redis.isConnected, |
|
|
latency: `${latency}ms` |
|
|
} |
|
|
} catch (error) { |
|
|
return { |
|
|
status: 'unhealthy', |
|
|
connected: false, |
|
|
error: error.message |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async checkLoggerHealth() { |
|
|
try { |
|
|
const health = logger.healthCheck() |
|
|
return { |
|
|
status: health.healthy ? 'healthy' : 'unhealthy', |
|
|
...health |
|
|
} |
|
|
} catch (error) { |
|
|
return { |
|
|
status: 'unhealthy', |
|
|
error: error.message |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
async start() { |
|
|
try { |
|
|
await this.initialize() |
|
|
|
|
|
this.server = this.app.listen(config.server.port, config.server.host, () => { |
|
|
logger.start( |
|
|
`🚀 Claude Relay Service started on ${config.server.host}:${config.server.port}` |
|
|
) |
|
|
logger.info( |
|
|
`🌐 Web interface: http://${config.server.host}:${config.server.port}/admin-next/api-stats` |
|
|
) |
|
|
logger.info( |
|
|
`🔗 API endpoint: http://${config.server.host}:${config.server.port}/api/v1/messages` |
|
|
) |
|
|
logger.info(`⚙️ Admin API: http://${config.server.host}:${config.server.port}/admin`) |
|
|
logger.info(`🏥 Health check: http://${config.server.host}:${config.server.port}/health`) |
|
|
logger.info(`📊 Metrics: http://${config.server.host}:${config.server.port}/metrics`) |
|
|
}) |
|
|
|
|
|
const serverTimeout = 600000 |
|
|
this.server.timeout = serverTimeout |
|
|
this.server.keepAliveTimeout = serverTimeout + 5000 |
|
|
logger.info(`⏱️ Server timeout set to ${serverTimeout}ms (${serverTimeout / 1000}s)`) |
|
|
|
|
|
|
|
|
this.startCleanupTasks() |
|
|
|
|
|
|
|
|
this.setupGracefulShutdown() |
|
|
} catch (error) { |
|
|
logger.error('💥 Failed to start server:', error) |
|
|
process.exit(1) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async initializeCacheMonitoring() { |
|
|
try { |
|
|
logger.info('🔄 Initializing cache monitoring...') |
|
|
|
|
|
|
|
|
const services = [ |
|
|
{ name: 'claudeAccount', service: require('./services/claudeAccountService') }, |
|
|
{ name: 'claudeConsole', service: require('./services/claudeConsoleAccountService') }, |
|
|
{ name: 'bedrockAccount', service: require('./services/bedrockAccountService') } |
|
|
] |
|
|
|
|
|
|
|
|
for (const { name, service } of services) { |
|
|
if (service && (service._decryptCache || service.decryptCache)) { |
|
|
const cache = service._decryptCache || service.decryptCache |
|
|
cacheMonitor.registerCache(`${name}_decrypt`, cache) |
|
|
logger.info(`✅ Registered ${name} decrypt cache for monitoring`) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
const stats = cacheMonitor.getGlobalStats() |
|
|
logger.info(`📊 Cache System - Registered: ${stats.cacheCount} caches`) |
|
|
}, 5000) |
|
|
|
|
|
logger.success('✅ Cache monitoring initialized') |
|
|
} catch (error) { |
|
|
logger.error('❌ Failed to initialize cache monitoring:', error) |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
startCleanupTasks() { |
|
|
|
|
|
setInterval(async () => { |
|
|
try { |
|
|
logger.info('🧹 Starting scheduled cleanup...') |
|
|
|
|
|
const apiKeyService = require('./services/apiKeyService') |
|
|
const claudeAccountService = require('./services/claudeAccountService') |
|
|
|
|
|
const [expiredKeys, errorAccounts] = await Promise.all([ |
|
|
apiKeyService.cleanupExpiredKeys(), |
|
|
claudeAccountService.cleanupErrorAccounts(), |
|
|
claudeAccountService.cleanupTempErrorAccounts() |
|
|
]) |
|
|
|
|
|
await redis.cleanup() |
|
|
|
|
|
logger.success( |
|
|
`🧹 Cleanup completed: ${expiredKeys} expired keys, ${errorAccounts} error accounts reset` |
|
|
) |
|
|
} catch (error) { |
|
|
logger.error('❌ Cleanup task failed:', error) |
|
|
} |
|
|
}, config.system.cleanupInterval) |
|
|
|
|
|
logger.info( |
|
|
`🔄 Cleanup tasks scheduled every ${config.system.cleanupInterval / 1000 / 60} minutes` |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
const rateLimitCleanupService = require('./services/rateLimitCleanupService') |
|
|
const cleanupIntervalMinutes = config.system.rateLimitCleanupInterval || 5 |
|
|
rateLimitCleanupService.start(cleanupIntervalMinutes) |
|
|
logger.info( |
|
|
`🚨 Rate limit cleanup service started (checking every ${cleanupIntervalMinutes} minutes)` |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
setInterval(async () => { |
|
|
try { |
|
|
const keys = await redis.keys('concurrency:*') |
|
|
if (keys.length === 0) { |
|
|
return |
|
|
} |
|
|
|
|
|
const now = Date.now() |
|
|
let totalCleaned = 0 |
|
|
|
|
|
|
|
|
for (const key of keys) { |
|
|
try { |
|
|
const cleaned = await redis.client.eval( |
|
|
` |
|
|
local key = KEYS[1] |
|
|
local now = tonumber(ARGV[1]) |
|
|
|
|
|
-- 清理过期项 |
|
|
redis.call('ZREMRANGEBYSCORE', key, '-inf', now) |
|
|
|
|
|
-- 获取剩余计数 |
|
|
local count = redis.call('ZCARD', key) |
|
|
|
|
|
-- 如果计数为0,删除键 |
|
|
if count <= 0 then |
|
|
redis.call('DEL', key) |
|
|
return 1 |
|
|
end |
|
|
|
|
|
return 0 |
|
|
`, |
|
|
1, |
|
|
key, |
|
|
now |
|
|
) |
|
|
if (cleaned === 1) { |
|
|
totalCleaned++ |
|
|
} |
|
|
} catch (error) { |
|
|
logger.error(`❌ Failed to clean concurrency key ${key}:`, error) |
|
|
} |
|
|
} |
|
|
|
|
|
if (totalCleaned > 0) { |
|
|
logger.info(`🔢 Concurrency cleanup: cleaned ${totalCleaned} expired keys`) |
|
|
} |
|
|
} catch (error) { |
|
|
logger.error('❌ Concurrency cleanup task failed:', error) |
|
|
} |
|
|
}, 60000) |
|
|
|
|
|
logger.info('🔢 Concurrency cleanup task started (running every 1 minute)') |
|
|
} |
|
|
|
|
|
setupGracefulShutdown() { |
|
|
const shutdown = async (signal) => { |
|
|
logger.info(`🛑 Received ${signal}, starting graceful shutdown...`) |
|
|
|
|
|
if (this.server) { |
|
|
this.server.close(async () => { |
|
|
logger.info('🚪 HTTP server closed') |
|
|
|
|
|
|
|
|
try { |
|
|
pricingService.cleanup() |
|
|
logger.info('💰 Pricing service cleaned up') |
|
|
} catch (error) { |
|
|
logger.error('❌ Error cleaning up pricing service:', error) |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const modelService = require('./services/modelService') |
|
|
modelService.cleanup() |
|
|
logger.info('📋 Model service cleaned up') |
|
|
} catch (error) { |
|
|
logger.error('❌ Error cleaning up model service:', error) |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const rateLimitCleanupService = require('./services/rateLimitCleanupService') |
|
|
rateLimitCleanupService.stop() |
|
|
logger.info('🚨 Rate limit cleanup service stopped') |
|
|
} catch (error) { |
|
|
logger.error('❌ Error stopping rate limit cleanup service:', error) |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
logger.info('🔢 Cleaning up all concurrency counters...') |
|
|
const keys = await redis.keys('concurrency:*') |
|
|
if (keys.length > 0) { |
|
|
await redis.client.del(...keys) |
|
|
logger.info(`✅ Cleaned ${keys.length} concurrency keys`) |
|
|
} else { |
|
|
logger.info('✅ No concurrency keys to clean') |
|
|
} |
|
|
} catch (error) { |
|
|
logger.error('❌ Error cleaning up concurrency counters:', error) |
|
|
|
|
|
} |
|
|
|
|
|
try { |
|
|
await redis.disconnect() |
|
|
logger.info('👋 Redis disconnected') |
|
|
} catch (error) { |
|
|
logger.error('❌ Error disconnecting Redis:', error) |
|
|
} |
|
|
|
|
|
logger.success('✅ Graceful shutdown completed') |
|
|
process.exit(0) |
|
|
}) |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
logger.warn('⚠️ Forced shutdown due to timeout') |
|
|
process.exit(1) |
|
|
}, 10000) |
|
|
} else { |
|
|
process.exit(0) |
|
|
} |
|
|
} |
|
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM')) |
|
|
process.on('SIGINT', () => shutdown('SIGINT')) |
|
|
|
|
|
|
|
|
process.on('uncaughtException', (error) => { |
|
|
logger.error('💥 Uncaught exception:', error) |
|
|
shutdown('uncaughtException') |
|
|
}) |
|
|
|
|
|
process.on('unhandledRejection', (reason, promise) => { |
|
|
logger.error('💥 Unhandled rejection at:', promise, 'reason:', reason) |
|
|
shutdown('unhandledRejection') |
|
|
}) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (require.main === module) { |
|
|
const app = new Application() |
|
|
app.start().catch((error) => { |
|
|
logger.error('💥 Application startup failed:', error) |
|
|
process.exit(1) |
|
|
}) |
|
|
} |
|
|
|
|
|
module.exports = Application |
|
|
|