Translator-API / central /src /index.js
NathMen12's picture
Update central/src/index.js
53e756c verified
Raw
History Blame Contribute Delete
9.57 kB
require('dotenv').config({ path: require('path').join(__dirname, '../../secrets/.env') });
const express = require('express');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const { WebSocketServer } = require('ws');
const http = require('http');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const PQueue = require('p-queue').default;
const os = require('os');
const winston = require('winston');
const settings = JSON.parse(fs.readFileSync(path.join(__dirname, '../settings.json'), 'utf8'));
// Logger configuration
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: path.join(__dirname, '../logs/central.log') })
]
});
// Metrics storage
const metrics = {
requests: [], // { timestamp, ip, duration, sourceLang, targetLang, inputText, outputText }
cpuHistory: [],
ramHistory: [],
workers: new Map() // workerId -> { id, ws, cpu, ram, lastHeartbeat, jobsActive }
};
// Job queue (local processing + worker dispatch)
const localQueue = new PQueue({ concurrency: settings.maxLocalJobs });
const workerQueue = new PQueue({ concurrency: 10 }); // dispatch to workers
// Express app
const app = express();
const server = http.createServer(app);
// WebSocket server for workers
const wss = new WebSocketServer({ server });
// Middleware
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.static(path.join(__dirname, '../../public')));
// Rate limiting
const limiter = rateLimit({
windowMs: settings.rateLimit.windowMs,
max: settings.rateLimit.maxRequestsPerMinutePerIP,
message: { error: 'Trop de requêtes, veuillez réessayer plus tard.' },
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req) => req.ip
});
app.use('/translate', limiter);
// Admin authentication middleware
const adminAuth = (req, res, next) => {
const adminCode = process.env.ADMIN_ACCESS_CODE;
const providedCode = req.headers['x-admin-code'] || req.query.admin_code;
if (!adminCode || providedCode !== adminCode) {
return res.status(401).json({ error: 'Code admin invalide' });
}
next();
};
// Load translation function
const translate = require('@vitalets/google-translate-api');
// Metrics collection
let prevCpu = process.cpuUsage();
let prevTime = process.hrtime.bigint();
setInterval(() => {
const cpuUsage = process.cpuUsage(prevCpu);
const currentTime = process.hrtime.bigint();
const elapsedNs = Number(currentTime - prevTime);
const elapsedUs = elapsedNs / 1000;
const cpuPercent = Math.min(100, ((cpuUsage.user + cpuUsage.system) / elapsedUs) * 100);
const memUsage = process.memoryUsage();
metrics.cpuHistory.push({ timestamp: Date.now(), cpu: Math.round(cpuPercent * 10) / 10 });
metrics.ramHistory.push({ timestamp: Date.now(), used: memUsage.heapUsed, total: memUsage.heapTotal });
prevCpu = process.cpuUsage();
prevTime = currentTime;
// Keep only last metricsWindowSeconds
const cutoff = Date.now() - settings.metricsWindowSeconds * 1000;
metrics.cpuHistory = metrics.cpuHistory.filter(m => m.timestamp > cutoff);
metrics.ramHistory = metrics.ramHistory.filter(m => m.timestamp > cutoff);
metrics.requests = metrics.requests.filter(m => m.timestamp > cutoff);
}, 1000);
// Clean up inactive workers
setInterval(() => {
const now = Date.now();
for (const [id, worker] of metrics.workers) {
if (now - worker.lastHeartbeat > 15000) {
logger.info(`Worker ${id} disconnected (timeout)`);
metrics.workers.delete(id);
}
}
}, 5000);
// WebSocket handling for workers
wss.on('connection', (ws, req) => {
const workerId = uuidv4();
logger.info(`Worker connected: ${workerId}`);
const worker = {
id: workerId,
ws,
cpu: 0,
ram: 0,
lastHeartbeat: Date.now(),
jobsActive: 0,
maxJobs: 2
};
metrics.workers.set(workerId, worker);
ws.on('message', (data) => {
try {
const msg = JSON.parse(data);
handleWorkerMessage(workerId, msg);
} catch (e) {
logger.error('Invalid worker message', e);
}
});
ws.on('close', () => {
logger.info(`Worker disconnected: ${workerId}`);
metrics.workers.delete(workerId);
});
ws.on('error', (err) => {
logger.error(`Worker ${workerId} error`, err);
});
// Send welcome message with config
ws.send(JSON.stringify({
type: 'welcome',
workerId,
maxConcurrentJobs: 2
}));
});
function handleWorkerMessage(workerId, msg) {
const worker = metrics.workers.get(workerId);
if (!worker) return;
switch (msg.type) {
case 'heartbeat':
worker.lastHeartbeat = Date.now();
worker.cpu = msg.cpu || 0;
worker.ram = msg.ram || 0;
worker.jobsActive = msg.jobsActive || 0;
break;
case 'result':
// Job completed by worker
if (msg.jobId) {
// Find and resolve the waiting promise
const pending = pendingJobs.get(msg.jobId);
if (pending) {
pending.resolve(msg.result);
pendingJobs.delete(msg.jobId);
}
}
worker.jobsActive = Math.max(0, worker.jobsActive - 1);
break;
case 'log':
logger.info(`Worker ${workerId} log: ${msg.message}`);
break;
}
}
// Pending jobs waiting for worker results
const pendingJobs = new Map();
// API Routes
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});
// Translate endpoint
app.post('/translate', async (req, res) => {
const startTime = Date.now();
const { text, source = 'auto', target = 'fr' } = req.body;
const ip = req.ip;
if (!text || typeof text !== 'string') {
return res.status(400).json({ error: 'Texte requis' });
}
if (text.length > 5000) {
return res.status(400).json({ error: 'Texte trop long (max 5000 caractères)' });
}
try {
// Try to dispatch to available worker first
let result;
const availableWorker = findAvailableWorker();
if (availableWorker) {
result = await dispatchToWorker(availableWorker, { text, source, target });
} else {
// Process locally
result = await localQueue.add(() => translateText(text, source, target));
}
const duration = Date.now() - startTime;
// Extract translated text from worker result object if needed
const translatedText = result.translatedText || result;
// Log request
metrics.requests.push({
timestamp: Date.now(),
ip,
duration,
sourceLang: source,
targetLang: target,
inputText: text.substring(0, 100),
outputText: translatedText.substring(0, 100)
});
res.json({ translatedText, source, target, duration });
} catch (error) {
logger.error('Translation error', error);
res.status(500).json({ error: 'Erreur de traduction' });
}
});
function translateText(text, source, target) {
return translate(text, { from: source, to: target }).then(res => res.text);
}
function findAvailableWorker() {
for (const [id, worker] of metrics.workers) {
if (worker.jobsActive < worker.maxJobs && worker.ws.readyState === 1) {
return worker;
}
}
return null;
}
function dispatchToWorker(worker, job) {
return new Promise((resolve, reject) => {
const jobId = uuidv4();
pendingJobs.set(jobId, { resolve, reject, timeout: setTimeout(() => {
pendingJobs.delete(jobId);
reject(new Error('Worker timeout'));
}, 30000) });
worker.ws.send(JSON.stringify({
type: 'translate',
jobId,
...job
}));
worker.jobsActive++;
});
}
// Admin dashboard page (auth handled client-side via modal)
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, '../../public/admin.html'));
});
app.get('/api/admin/metrics', adminAuth, (req, res) => {
const workers = [];
for (const [id, w] of metrics.workers) {
workers.push({
id,
cpu: w.cpu,
ram: w.ram,
jobsActive: w.jobsActive,
maxJobs: w.maxJobs,
lastHeartbeat: w.lastHeartbeat
});
}
res.json({
workers,
cpuHistory: metrics.cpuHistory,
ramHistory: metrics.ramHistory,
requests: metrics.requests.slice(-200), // Last 200 requests
requestRate: calculateRequestRate()
});
});
app.get('/api/admin/logs', adminAuth, (req, res) => {
// Return recent logs from metrics.requests
const logs = metrics.requests.slice(-100).map(r => ({
timestamp: r.timestamp,
ip: r.ip,
duration: r.duration,
sourceLang: r.sourceLang,
targetLang: r.targetLang,
inputText: r.inputText,
outputText: r.outputText
}));
res.json({ logs });
});
function calculateRequestRate() {
const now = Date.now();
const windowMs = 120000; // 2 minutes
const recent = metrics.requests.filter(r => r.timestamp > now - windowMs);
return recent.length;
}
// Main page
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../../public/index.html'));
});
// Wiki page
app.get('/wiki', (req, res) => {
res.sendFile(path.join(__dirname, '../../public/wiki.html'));
});
// Start server
const PORT = process.env.PORT || settings.port;
server.listen(PORT, '0.0.0.0', () => {
logger.info(`Central server running on port ${PORT}`);
logger.info(`Admin panel: http://localhost:${PORT}/admin`);
logger.info(`API: http://localhost:${PORT}/translate`);
});