modified: main_dc/valitovgaziz/analytics/package.json
modified: main_dc/valitovgaziz/analytics/server.js change dependencies
This commit is contained in:
@@ -14,7 +14,6 @@
|
|||||||
"helmet": "^7.0.0",
|
"helmet": "^7.0.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"rate-limiter-flexible": "^3.0.8",
|
|
||||||
"winston": "^3.10.0"
|
"winston": "^3.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -3,19 +3,41 @@ const cors = require('cors');
|
|||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const morgan = require('morgan');
|
const morgan = require('morgan');
|
||||||
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
// Настройка лимитера запросов
|
// Простой rate limiting
|
||||||
const rateLimiter = new RateLimiterMemory({
|
const requestCounts = new Map();
|
||||||
keyGenerator: (req) => req.ip,
|
const RATE_LIMIT = 100; // 100 запросов
|
||||||
points: 100, // 100 запросов
|
const RATE_LIMIT_WINDOW = 60000; // за 1 минуту
|
||||||
duration: 60, // за 60 секунд
|
|
||||||
});
|
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
|
// Middleware
|
||||||
app.use(helmet({
|
app.use(helmet({
|
||||||
@@ -25,50 +47,22 @@ app.use(compression());
|
|||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json({ limit: '1mb' }));
|
app.use(express.json({ limit: '1mb' }));
|
||||||
|
|
||||||
// Логирование запросов
|
// Логирование запросов в файл
|
||||||
const accessLogStream = require('fs').createWriteStream(
|
const accessLogStream = require('fs').createWriteStream(
|
||||||
path.join(__dirname, 'logs', 'access.log'),
|
path.join(__dirname, 'logs', 'access.log'),
|
||||||
{ flags: 'a' }
|
{ flags: 'a' }
|
||||||
);
|
);
|
||||||
app.use(morgan('combined', { stream: accessLogStream }));
|
app.use(morgan('combined', { stream: accessLogStream }));
|
||||||
|
|
||||||
// Winston логгер для приложения
|
// Простой логгер
|
||||||
const winston = require('winston');
|
const logger = {
|
||||||
const logger = winston.createLogger({
|
info: (message, meta = {}) => {
|
||||||
level: process.env.LOG_LEVEL || 'info',
|
console.log(`[INFO] ${message}`, JSON.stringify(meta));
|
||||||
format: winston.format.combine(
|
},
|
||||||
winston.format.timestamp(),
|
error: (message, meta = {}) => {
|
||||||
winston.format.json()
|
console.error(`[ERROR] ${message}`, JSON.stringify(meta));
|
||||||
),
|
|
||||||
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)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// Функция для записи аналитики в файл
|
// Функция для записи аналитики в файл
|
||||||
async function writeAnalytics(data) {
|
async function writeAnalytics(data) {
|
||||||
@@ -100,6 +94,8 @@ async function writeAnalytics(data) {
|
|||||||
eventsCount: data.events?.length || 1,
|
eventsCount: data.events?.length || 1,
|
||||||
sessionId: data.sessionId
|
sessionId: data.sessionId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error writing analytics data', { error: error.message });
|
logger.error('Error writing analytics data', { error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
@@ -111,11 +107,12 @@ app.get('/health', (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
timestamp: new Date().toISOString(),
|
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 {
|
try {
|
||||||
const { events, sessionId } = req.body;
|
const { events, sessionId } = req.body;
|
||||||
|
|
||||||
@@ -132,6 +129,11 @@ app.post('/api/analytics', async (req, res) => {
|
|||||||
received: events.length,
|
received: events.length,
|
||||||
sessionId
|
sessionId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.info('Analytics data received', {
|
||||||
|
count: events.length,
|
||||||
|
sessionId: sessionId
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Analytics processing error', { error: error.message });
|
logger.error('Analytics processing error', { error: error.message });
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -141,14 +143,18 @@ app.post('/api/analytics', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Статистика (только для разработки)
|
// Статистика
|
||||||
app.get('/api/stats', async (req, res) => {
|
app.get('/api/stats', async (req, res) => {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
return res.status(403).json({ error: 'Forbidden' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dataDir = path.join(__dirname, 'data');
|
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 files = await fs.readdir(dataDir);
|
||||||
const analyticsFiles = files.filter(f => f.startsWith('analytics-'));
|
const analyticsFiles = files.filter(f => f.startsWith('analytics-'));
|
||||||
|
|
||||||
@@ -156,6 +162,7 @@ app.get('/api/stats', async (req, res) => {
|
|||||||
const sessions = new Set();
|
const sessions = new Set();
|
||||||
|
|
||||||
for (const file of analyticsFiles.slice(-7)) { // Последние 7 дней
|
for (const file of analyticsFiles.slice(-7)) { // Последние 7 дней
|
||||||
|
try {
|
||||||
const content = await fs.readFile(path.join(dataDir, file), 'utf8');
|
const content = await fs.readFile(path.join(dataDir, file), 'utf8');
|
||||||
const data = JSON.parse(content);
|
const data = JSON.parse(content);
|
||||||
|
|
||||||
@@ -163,18 +170,37 @@ app.get('/api/stats', async (req, res) => {
|
|||||||
totalEvents += entry.events?.length || 0;
|
totalEvents += entry.events?.length || 0;
|
||||||
if (entry.sessionId) sessions.add(entry.sessionId);
|
if (entry.sessionId) sessions.add(entry.sessionId);
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error reading analytics file', { file, error: error.message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
totalEvents,
|
totalEvents,
|
||||||
uniqueSessions: sessions.size,
|
uniqueSessions: sessions.size,
|
||||||
filesCount: analyticsFiles.length
|
filesCount: analyticsFiles.length,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error('Stats error', { error: error.message });
|
||||||
res.status(500).json({ 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
|
// Обработка 404
|
||||||
app.use((req, res) => {
|
app.use((req, res) => {
|
||||||
res.status(404).json({ error: 'Route not found' });
|
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' });
|
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}`);
|
logger.info(`Analytics server running on port ${PORT}`);
|
||||||
console.log(`🔍 Analytics server: http://localhost:${PORT}`);
|
console.log(`🔍 Analytics server: http://localhost:${PORT}`);
|
||||||
|
console.log(`📊 Health check: http://localhost:${PORT}/health`);
|
||||||
|
console.log(`📈 Stats: http://localhost:${PORT}/api/stats`);
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user