diff --git a/main_dc/valitovgaziz/analytics/package.json b/main_dc/valitovgaziz/analytics/package.json index 228fbd0..39d6eea 100644 --- a/main_dc/valitovgaziz/analytics/package.json +++ b/main_dc/valitovgaziz/analytics/package.json @@ -14,7 +14,6 @@ "helmet": "^7.0.0", "morgan": "^1.10.0", "compression": "^1.7.4", - "rate-limiter-flexible": "^3.0.8", "winston": "^3.10.0" }, "devDependencies": { diff --git a/main_dc/valitovgaziz/analytics/server.js b/main_dc/valitovgaziz/analytics/server.js index 7951673..718528a 100644 --- a/main_dc/valitovgaziz/analytics/server.js +++ b/main_dc/valitovgaziz/analytics/server.js @@ -3,19 +3,41 @@ const cors = require('cors'); const helmet = require('helmet'); const compression = require('compression'); const morgan = require('morgan'); -const { RateLimiterMemory } = require('rate-limiter-flexible'); const fs = require('fs').promises; const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; -// Настройка лимитера запросов -const rateLimiter = new RateLimiterMemory({ - keyGenerator: (req) => req.ip, - points: 100, // 100 запросов - duration: 60, // за 60 секунд -}); +// Простой rate limiting +const requestCounts = new Map(); +const RATE_LIMIT = 100; // 100 запросов +const RATE_LIMIT_WINDOW = 60000; // за 1 минуту + +function rateLimit(req, res, next) { + const ip = req.ip; + const now = Date.now(); + const windowStart = now - RATE_LIMIT_WINDOW; + + if (!requestCounts.has(ip)) { + requestCounts.set(ip, []); + } + + const requests = requestCounts.get(ip); + // Удаляем старые запросы + const recentRequests = requests.filter(time => time > windowStart); + + if (recentRequests.length >= RATE_LIMIT) { + return res.status(429).json({ + error: 'Too Many Requests', + retryAfter: Math.ceil((requests[0] + RATE_LIMIT_WINDOW - now) / 1000) + }); + } + + recentRequests.push(now); + requestCounts.set(ip, recentRequests); + next(); +} // Middleware app.use(helmet({ @@ -25,50 +47,22 @@ app.use(compression()); app.use(cors()); app.use(express.json({ limit: '1mb' })); -// Логирование запросов +// Логирование запросов в файл const accessLogStream = require('fs').createWriteStream( path.join(__dirname, 'logs', 'access.log'), { flags: 'a' } ); app.use(morgan('combined', { stream: accessLogStream })); -// Winston логгер для приложения -const winston = require('winston'); -const logger = winston.createLogger({ - level: process.env.LOG_LEVEL || 'info', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ), - transports: [ - new winston.transports.File({ - filename: path.join(__dirname, 'logs', 'error.log'), - level: 'error' - }), - new winston.transports.File({ - filename: path.join(__dirname, 'logs', 'combined.log') - }), - ], -}); - -if (process.env.NODE_ENV !== 'production') { - logger.add(new winston.transports.Console({ - format: winston.format.simple() - })); -} - -// Middleware для ограничения запросов -app.use(async (req, res, next) => { - try { - await rateLimiter.consume(req.ip); - next(); - } catch (rejRes) { - res.status(429).json({ - error: 'Too Many Requests', - retryAfter: Math.ceil(rejRes.msBeforeNext / 1000) - }); +// Простой логгер +const logger = { + info: (message, meta = {}) => { + console.log(`[INFO] ${message}`, JSON.stringify(meta)); + }, + error: (message, meta = {}) => { + console.error(`[ERROR] ${message}`, JSON.stringify(meta)); } -}); +}; // Функция для записи аналитики в файл async function writeAnalytics(data) { @@ -100,6 +94,8 @@ async function writeAnalytics(data) { eventsCount: data.events?.length || 1, sessionId: data.sessionId }); + + return true; } catch (error) { logger.error('Error writing analytics data', { error: error.message }); throw error; @@ -111,11 +107,12 @@ app.get('/health', (req, res) => { res.json({ status: 'OK', timestamp: new Date().toISOString(), - service: 'analytics-server' + service: 'analytics-server', + version: '1.0.0' }); }); -app.post('/api/analytics', async (req, res) => { +app.post('/api/analytics', rateLimit, async (req, res) => { try { const { events, sessionId } = req.body; @@ -132,6 +129,11 @@ app.post('/api/analytics', async (req, res) => { received: events.length, sessionId }); + + logger.info('Analytics data received', { + count: events.length, + sessionId: sessionId + }); } catch (error) { logger.error('Analytics processing error', { error: error.message }); res.status(500).json({ @@ -141,14 +143,18 @@ app.post('/api/analytics', async (req, res) => { } }); -// Статистика (только для разработки) +// Статистика app.get('/api/stats', async (req, res) => { - if (process.env.NODE_ENV === 'production') { - return res.status(403).json({ error: 'Forbidden' }); - } - try { const dataDir = path.join(__dirname, 'data'); + + // Проверяем существует ли директория + try { + await fs.access(dataDir); + } catch { + return res.json({ totalEvents: 0, filesCount: 0, uniqueSessions: 0 }); + } + const files = await fs.readdir(dataDir); const analyticsFiles = files.filter(f => f.startsWith('analytics-')); @@ -156,25 +162,45 @@ app.get('/api/stats', async (req, res) => { const sessions = new Set(); for (const file of analyticsFiles.slice(-7)) { // Последние 7 дней - const content = await fs.readFile(path.join(dataDir, file), 'utf8'); - const data = JSON.parse(content); + try { + const content = await fs.readFile(path.join(dataDir, file), 'utf8'); + const data = JSON.parse(content); - data.forEach(entry => { - totalEvents += entry.events?.length || 0; - if (entry.sessionId) sessions.add(entry.sessionId); - }); + data.forEach(entry => { + totalEvents += entry.events?.length || 0; + if (entry.sessionId) sessions.add(entry.sessionId); + }); + } catch (error) { + logger.error('Error reading analytics file', { file, error: error.message }); + } } res.json({ totalEvents, uniqueSessions: sessions.size, - filesCount: analyticsFiles.length + filesCount: analyticsFiles.length, + lastUpdated: new Date().toISOString() }); } catch (error) { + logger.error('Stats error', { error: error.message }); res.status(500).json({ error: error.message }); } }); +// Информация о сервисе +app.get('/api/info', (req, res) => { + res.json({ + service: 'analytics-server', + version: '1.0.0', + status: 'running', + endpoints: [ + 'POST /api/analytics - Send analytics events', + 'GET /api/stats - Get statistics', + 'GET /health - Health check' + ] + }); +}); + // Обработка 404 app.use((req, res) => { res.status(404).json({ error: 'Route not found' }); @@ -186,7 +212,21 @@ app.use((error, req, res, next) => { res.status(500).json({ error: 'Internal server error' }); }); -app.listen(PORT, '0.0.0.0', () => { +// Создаем необходимые директории при запуске +async function initialize() { + try { + await fs.mkdir(path.join(__dirname, 'logs'), { recursive: true }); + await fs.mkdir(path.join(__dirname, 'data'), { recursive: true }); + logger.info('Directories initialized'); + } catch (error) { + logger.error('Directory initialization failed', { error: error.message }); + } +} + +app.listen(PORT, '0.0.0.0', async () => { + await initialize(); logger.info(`Analytics server running on port ${PORT}`); console.log(`🔍 Analytics server: http://localhost:${PORT}`); + console.log(`📊 Health check: http://localhost:${PORT}/health`); + console.log(`📈 Stats: http://localhost:${PORT}/api/stats`); }); \ No newline at end of file