feat(kernel): implementação completa do Kernel Nativo - Semana 3
Kernel Arcadia - Sistema nativo de gerenciamento de serviços: Core: - ProcessManager: spawn, monitor, restart automático de serviços - HealthMonitor: HTTP health checks com circuit breaker - LogAggregator: coleta e streaming de logs API & Dashboard: - API REST /api/kernel/* para controle dos serviços - Dashboard Web na porta 5001 - WebSocket /ws/kernel para updates em tempo real Serviços configurados (6): - Python: Contabil (8003), BI Engine (8004), Automation (8005) - Node.js: Communication (9001), Core API (9002), Worker (9003) Integração: - server/index.ts integrado com flag KERNEL_ENABLED - Build passando, pronto para testes Uso: KERNEL_ENABLED=true npm run dev Acesse http://localhost:5001 para o dashboard
This commit is contained in:
parent
201580a47a
commit
849934dae8
|
|
@ -8,6 +8,7 @@ import { createServer } from "http";
|
||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { logger, httpLogger } from "./logger";
|
import { logger, httpLogger } from "./logger";
|
||||||
|
import { ArcadiaKernel } from "./kernel"; // Kernel Nativo - Semana 3
|
||||||
|
|
||||||
interface ManagedService {
|
interface ManagedService {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -397,4 +398,21 @@ export function log(message: string, source = "express") {
|
||||||
log(`serving on port ${port}`);
|
log(`serving on port ${port}`);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Inicia Arcadia Kernel (Dashboard porta 5001) - Semana 3
|
||||||
|
// Só inicia se KERNEL_ENABLED estiver setado
|
||||||
|
if (process.env.KERNEL_ENABLED === 'true') {
|
||||||
|
try {
|
||||||
|
const kernel = new ArcadiaKernel({
|
||||||
|
dashboard: { enabled: true, port: 5001, host: '0.0.0.0' },
|
||||||
|
autoStart: process.env.KERNEL_AUTOSTART === 'true',
|
||||||
|
});
|
||||||
|
await kernel.start();
|
||||||
|
log('Arcadia Kernel iniciado em http://0.0.0.0:5001');
|
||||||
|
} catch (error) {
|
||||||
|
log(`Erro ao iniciar Kernel: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log('Kernel desabilitado (set KERNEL_ENABLED=true para ativar)');
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
/**
|
||||||
|
* API Routes - Rotas REST do Kernel
|
||||||
|
* Endpoints para controle e monitoramento dos serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import { ProcessManager } from '../core/ProcessManager';
|
||||||
|
import { HealthMonitor } from '../core/HealthMonitor';
|
||||||
|
import { LogAggregator } from '../core/LogAggregator';
|
||||||
|
import { ApiResponse, ServiceActionRequest, ServiceLogsRequest } from '../types';
|
||||||
|
|
||||||
|
export function createKernelRoutes(
|
||||||
|
processManager: ProcessManager,
|
||||||
|
healthMonitor: HealthMonitor,
|
||||||
|
logAggregator: LogAggregator
|
||||||
|
): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// Middleware para JSON
|
||||||
|
router.use(expressJson());
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SERVICES ENDPOINTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/services
|
||||||
|
* Lista todos os serviços com seu estado atual
|
||||||
|
*/
|
||||||
|
router.get('/services', (req: Request, res: Response) => {
|
||||||
|
const states = processManager.getAllStates();
|
||||||
|
|
||||||
|
// Adiciona health status de cada serviço
|
||||||
|
const servicesWithHealth = states.map(state => ({
|
||||||
|
...state,
|
||||||
|
health: healthMonitor.getHealth(state.config.id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(successResponse(servicesWithHealth));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/services/:id
|
||||||
|
* Detalhes de um serviço específico
|
||||||
|
*/
|
||||||
|
router.get('/services/:id', (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const state = processManager.getServiceState(id);
|
||||||
|
|
||||||
|
if (!state) {
|
||||||
|
return res.status(404).json(errorResponse(`Serviço ${id} não encontrado`));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
...state,
|
||||||
|
health: healthMonitor.getHealth(id),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/services/:id/start
|
||||||
|
* Inicia um serviço
|
||||||
|
*/
|
||||||
|
router.post('/services/:id/start', async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await processManager.startService(id);
|
||||||
|
healthMonitor.startMonitoring(id, state);
|
||||||
|
res.json(successResponse(state));
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(errorResponse(
|
||||||
|
error instanceof Error ? error.message : 'Erro ao iniciar serviço'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/services/:id/stop
|
||||||
|
* Para um serviço
|
||||||
|
*/
|
||||||
|
router.post('/services/:id/stop', async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { force } = req.body as ServiceActionRequest;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await processManager.stopService(id, force);
|
||||||
|
healthMonitor.stopMonitoring(id);
|
||||||
|
res.json(successResponse(state));
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(errorResponse(
|
||||||
|
error instanceof Error ? error.message : 'Erro ao parar serviço'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/services/:id/restart
|
||||||
|
* Reinicia um serviço
|
||||||
|
*/
|
||||||
|
router.post('/services/:id/restart', async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await processManager.restartService(id);
|
||||||
|
healthMonitor.startMonitoring(id, state);
|
||||||
|
res.json(successResponse(state));
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(errorResponse(
|
||||||
|
error instanceof Error ? error.message : 'Erro ao reiniciar serviço'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/services/:id/kill
|
||||||
|
* Mata um serviço (force)
|
||||||
|
*/
|
||||||
|
router.post('/services/:id/kill', async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await processManager.killService(id);
|
||||||
|
healthMonitor.stopMonitoring(id);
|
||||||
|
res.json(successResponse(state));
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json(errorResponse(
|
||||||
|
error instanceof Error ? error.message : 'Erro ao matar serviço'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// LOGS ENDPOINTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/services/:id/logs
|
||||||
|
* Logs de um serviço específico
|
||||||
|
*/
|
||||||
|
router.get('/services/:id/logs', (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { lines = '100', level } = req.query;
|
||||||
|
|
||||||
|
const logs = logAggregator.getServiceLogs(
|
||||||
|
id,
|
||||||
|
parseInt(lines as string, 10) || 100
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filtra por nível se especificado
|
||||||
|
let filteredLogs = logs;
|
||||||
|
if (level) {
|
||||||
|
const levels = ['debug', 'info', 'warn', 'error'];
|
||||||
|
const minLevel = levels.indexOf(level as string);
|
||||||
|
filteredLogs = logs.filter(l => levels.indexOf(l.level) >= minLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(successResponse(filteredLogs));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/logs
|
||||||
|
* Logs de todos os serviços
|
||||||
|
*/
|
||||||
|
router.get('/logs', (req: Request, res: Response) => {
|
||||||
|
const { lines = '100', service, level } = req.query;
|
||||||
|
|
||||||
|
const logs = logAggregator.getAll({
|
||||||
|
lines: parseInt(lines as string, 10) || 100,
|
||||||
|
serviceId: service as string | undefined,
|
||||||
|
level: level as any,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(successResponse(logs));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/kernel/logs
|
||||||
|
* Limpa logs
|
||||||
|
*/
|
||||||
|
router.delete('/logs', (req: Request, res: Response) => {
|
||||||
|
const { service } = req.query;
|
||||||
|
|
||||||
|
if (service) {
|
||||||
|
logAggregator.clearService(service as string);
|
||||||
|
} else {
|
||||||
|
logAggregator.clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(successResponse({ cleared: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// HEALTH ENDPOINTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/health
|
||||||
|
* Health check geral do kernel
|
||||||
|
*/
|
||||||
|
router.get('/health', (req: Request, res: Response) => {
|
||||||
|
const states = processManager.getAllStates();
|
||||||
|
const allHealth = healthMonitor.getAllHealth();
|
||||||
|
|
||||||
|
const healthy = Array.from(allHealth.values()).filter(h => h === 'healthy').length;
|
||||||
|
const unhealthy = Array.from(allHealth.values()).filter(h => h === 'unhealthy').length;
|
||||||
|
const running = states.filter(s => s.status === 'running').length;
|
||||||
|
const total = states.length;
|
||||||
|
|
||||||
|
const health = {
|
||||||
|
status: unhealthy === 0 ? 'healthy' : 'unhealthy',
|
||||||
|
summary: {
|
||||||
|
total,
|
||||||
|
running,
|
||||||
|
healthy,
|
||||||
|
unhealthy,
|
||||||
|
unknown: total - healthy - unhealthy,
|
||||||
|
},
|
||||||
|
services: states.map(s => ({
|
||||||
|
id: s.config.id,
|
||||||
|
name: s.config.name,
|
||||||
|
status: s.status,
|
||||||
|
health: allHealth.get(s.config.id) || 'unknown',
|
||||||
|
pid: s.pid,
|
||||||
|
uptime: s.startTime
|
||||||
|
? Date.now() - s.startTime.getTime()
|
||||||
|
: 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(successResponse(health));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/services/:id/health
|
||||||
|
* Health check de um serviço específico
|
||||||
|
*/
|
||||||
|
router.get('/services/:id/health', async (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
// Força um check imediato
|
||||||
|
const health = await healthMonitor.forceCheck(id);
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
serviceId: id,
|
||||||
|
health,
|
||||||
|
timestamp: new Date(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// ACTIONS ENDPOINTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/actions/start-all
|
||||||
|
* Inicia todos os serviços
|
||||||
|
*/
|
||||||
|
router.post('/actions/start-all', async (req: Request, res: Response) => {
|
||||||
|
const { auto = true } = req.body;
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
message: 'Iniciando serviços...',
|
||||||
|
auto,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Executa em background
|
||||||
|
processManager.startAll(auto).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/actions/stop-all
|
||||||
|
* Para todos os serviços
|
||||||
|
*/
|
||||||
|
router.post('/actions/stop-all', async (req: Request, res: Response) => {
|
||||||
|
const { force = false } = req.body;
|
||||||
|
|
||||||
|
await processManager.stopAll(force);
|
||||||
|
healthMonitor.stopAll();
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
message: 'Todos os serviços parados',
|
||||||
|
force,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/kernel/actions/restart-all
|
||||||
|
* Reinicia todos os serviços
|
||||||
|
*/
|
||||||
|
router.post('/actions/restart-all', async (req: Request, res: Response) => {
|
||||||
|
const states = processManager.getAllStates();
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
message: 'Reiniciando serviços...',
|
||||||
|
count: states.filter(s => s.status === 'running').length,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Executa em background
|
||||||
|
for (const state of states) {
|
||||||
|
if (state.status === 'running') {
|
||||||
|
try {
|
||||||
|
await processManager.restartService(state.config.id);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignora erros individuais
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// STATS ENDPOINTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/stats
|
||||||
|
* Estatísticas do kernel
|
||||||
|
*/
|
||||||
|
router.get('/stats', (req: Request, res: Response) => {
|
||||||
|
const states = processManager.getAllStates();
|
||||||
|
const logStats = logAggregator.getStats();
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
services: {
|
||||||
|
total: states.length,
|
||||||
|
running: states.filter(s => s.status === 'running').length,
|
||||||
|
stopped: states.filter(s => s.status === 'stopped').length,
|
||||||
|
error: states.filter(s => s.status === 'error').length,
|
||||||
|
restarting: states.filter(s => s.status === 'restarting').length,
|
||||||
|
},
|
||||||
|
health: {
|
||||||
|
healthy: Array.from(healthMonitor.getAllHealth().values())
|
||||||
|
.filter(h => h === 'healthy').length,
|
||||||
|
unhealthy: Array.from(healthMonitor.getAllHealth().values())
|
||||||
|
.filter(h => h === 'unhealthy').length,
|
||||||
|
unknown: Array.from(healthMonitor.getAllHealth().values())
|
||||||
|
.filter(h => h === 'unknown').length,
|
||||||
|
},
|
||||||
|
logs: logStats,
|
||||||
|
totalRestarts: states.reduce((sum, s) => sum + s.restartCount, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(successResponse(stats));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// MISC ENDPOINTS
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/kernel/config
|
||||||
|
* Configuração do kernel
|
||||||
|
*/
|
||||||
|
router.get('/config', (req: Request, res: Response) => {
|
||||||
|
const states = processManager.getAllStates();
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
services: states.map(s => ({
|
||||||
|
id: s.config.id,
|
||||||
|
name: s.config.name,
|
||||||
|
type: s.config.type,
|
||||||
|
port: s.config.port,
|
||||||
|
autoStart: s.config.autoStart,
|
||||||
|
maxRestarts: s.config.maxRestarts,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper para JSON middleware (evita import do express inteiro)
|
||||||
|
function expressJson() {
|
||||||
|
return (req: Request, res: Response, next: any) => {
|
||||||
|
if (req.headers['content-type']?.includes('application/json')) {
|
||||||
|
let data = '';
|
||||||
|
req.on('data', chunk => data += chunk);
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
(req as any).body = data ? JSON.parse(data) : {};
|
||||||
|
} catch {
|
||||||
|
(req as any).body = {};
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
(req as any).body = {};
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers de resposta
|
||||||
|
function successResponse<T>(data: T): ApiResponse<T> {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorResponse(error: string): ApiResponse {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error,
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
{
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"id": "python-contabil",
|
||||||
|
"name": "Contabil Service",
|
||||||
|
"description": "Serviço de contabilidade e integração fiscal",
|
||||||
|
"type": "python",
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["-m", "uvicorn", "contabil.main:app", "--host", "0.0.0.0", "--port", "8003"],
|
||||||
|
"cwd": "./python-service",
|
||||||
|
"port": 8003,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/health",
|
||||||
|
"interval": 30000,
|
||||||
|
"timeout": 5000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": ".",
|
||||||
|
"SERVICE_NAME": "contabil",
|
||||||
|
"LOG_LEVEL": "info"
|
||||||
|
},
|
||||||
|
"maxRestarts": 5,
|
||||||
|
"autoStart": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "python-bi",
|
||||||
|
"name": "BI Engine",
|
||||||
|
"description": "Motor de Business Intelligence e análise de dados",
|
||||||
|
"type": "python",
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["-m", "uvicorn", "bi_engine.main:app", "--host", "0.0.0.0", "--port", "8004"],
|
||||||
|
"cwd": "./python-service",
|
||||||
|
"port": 8004,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/health",
|
||||||
|
"interval": 30000,
|
||||||
|
"timeout": 5000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": ".",
|
||||||
|
"SERVICE_NAME": "bi",
|
||||||
|
"LOG_LEVEL": "info"
|
||||||
|
},
|
||||||
|
"maxRestarts": 5,
|
||||||
|
"autoStart": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "python-automation",
|
||||||
|
"name": "Automation Service",
|
||||||
|
"description": "Serviço de automações e workflows",
|
||||||
|
"type": "python",
|
||||||
|
"command": "python3",
|
||||||
|
"args": ["-m", "uvicorn", "automation.main:app", "--host", "0.0.0.0", "--port", "8005"],
|
||||||
|
"cwd": "./python-service",
|
||||||
|
"port": 8005,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/health",
|
||||||
|
"interval": 30000,
|
||||||
|
"timeout": 5000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": ".",
|
||||||
|
"SERVICE_NAME": "automation",
|
||||||
|
"LOG_LEVEL": "info"
|
||||||
|
},
|
||||||
|
"maxRestarts": 5,
|
||||||
|
"autoStart": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node-communication",
|
||||||
|
"name": "Communication Service",
|
||||||
|
"description": "Serviço de comunicação WhatsApp e outros canais",
|
||||||
|
"type": "node",
|
||||||
|
"command": "node",
|
||||||
|
"args": ["dist/services/communication/index.js"],
|
||||||
|
"port": 9001,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/health",
|
||||||
|
"interval": 30000,
|
||||||
|
"timeout": 5000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_NAME": "communication",
|
||||||
|
"PORT": "9001"
|
||||||
|
},
|
||||||
|
"maxRestarts": 5,
|
||||||
|
"autoStart": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node-core-api",
|
||||||
|
"name": "Core API Service",
|
||||||
|
"description": "API Core adicional para processamento",
|
||||||
|
"type": "node",
|
||||||
|
"command": "node",
|
||||||
|
"args": ["dist/services/core-api/index.js"],
|
||||||
|
"port": 9002,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/health",
|
||||||
|
"interval": 30000,
|
||||||
|
"timeout": 5000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_NAME": "core-api",
|
||||||
|
"PORT": "9002"
|
||||||
|
},
|
||||||
|
"maxRestarts": 5,
|
||||||
|
"autoStart": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node-worker",
|
||||||
|
"name": "Worker Service",
|
||||||
|
"description": "Processamento de jobs e tarefas em background",
|
||||||
|
"type": "node",
|
||||||
|
"command": "node",
|
||||||
|
"args": ["dist/services/worker/index.js"],
|
||||||
|
"port": 9003,
|
||||||
|
"healthCheck": {
|
||||||
|
"path": "/health",
|
||||||
|
"interval": 30000,
|
||||||
|
"timeout": 5000,
|
||||||
|
"retries": 3
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"NODE_ENV": "production",
|
||||||
|
"SERVICE_NAME": "worker",
|
||||||
|
"PORT": "9003"
|
||||||
|
},
|
||||||
|
"maxRestarts": 5,
|
||||||
|
"autoStart": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
/**
|
||||||
|
* HealthMonitor - Monitor de saúde dos serviços
|
||||||
|
* Faz health checks HTTP periódicos nos serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { ServiceState, HealthStatus, HealthCheckConfig } from '../types';
|
||||||
|
|
||||||
|
export interface HealthMonitorOptions {
|
||||||
|
defaultInterval?: number;
|
||||||
|
defaultTimeout?: number;
|
||||||
|
defaultRetries?: number;
|
||||||
|
onHealthChange?: (serviceId: string, health: HealthStatus, lastError?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HealthCheckState {
|
||||||
|
serviceId: string;
|
||||||
|
config: HealthCheckConfig;
|
||||||
|
status: HealthStatus;
|
||||||
|
consecutiveFailures: number;
|
||||||
|
consecutiveSuccesses: number;
|
||||||
|
lastCheck?: Date;
|
||||||
|
lastError?: string;
|
||||||
|
timer?: NodeJS.Timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HealthMonitor extends EventEmitter {
|
||||||
|
private checks: Map<string, HealthCheckState> = new Map();
|
||||||
|
private options: Required<HealthMonitorOptions>;
|
||||||
|
|
||||||
|
constructor(options: HealthMonitorOptions = {}) {
|
||||||
|
super();
|
||||||
|
this.options = {
|
||||||
|
defaultInterval: 30000,
|
||||||
|
defaultTimeout: 5000,
|
||||||
|
defaultRetries: 3,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia monitoramento de um serviço
|
||||||
|
*/
|
||||||
|
startMonitoring(serviceId: string, state: ServiceState): void {
|
||||||
|
if (!state.config.healthCheck) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para monitoramento anterior se existir
|
||||||
|
this.stopMonitoring(serviceId);
|
||||||
|
|
||||||
|
const checkState: HealthCheckState = {
|
||||||
|
serviceId,
|
||||||
|
config: {
|
||||||
|
interval: this.options.defaultInterval,
|
||||||
|
timeout: this.options.defaultTimeout,
|
||||||
|
retries: this.options.defaultRetries,
|
||||||
|
...state.config.healthCheck,
|
||||||
|
},
|
||||||
|
status: 'unknown',
|
||||||
|
consecutiveFailures: 0,
|
||||||
|
consecutiveSuccesses: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.checks.set(serviceId, checkState);
|
||||||
|
|
||||||
|
// Primeiro check imediato
|
||||||
|
this.performCheck(checkState);
|
||||||
|
|
||||||
|
// Agenda checks periódicos
|
||||||
|
this.scheduleNextCheck(checkState);
|
||||||
|
|
||||||
|
this.emit('monitoring:started', { serviceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para monitoramento de um serviço
|
||||||
|
*/
|
||||||
|
stopMonitoring(serviceId: string): void {
|
||||||
|
const check = this.checks.get(serviceId);
|
||||||
|
if (check) {
|
||||||
|
if (check.timer) {
|
||||||
|
clearTimeout(check.timer);
|
||||||
|
}
|
||||||
|
this.checks.delete(serviceId);
|
||||||
|
this.emit('monitoring:stopped', { serviceId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para todos os monitoramentos
|
||||||
|
*/
|
||||||
|
stopAll(): void {
|
||||||
|
for (const [serviceId] of this.checks) {
|
||||||
|
this.stopMonitoring(serviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna status de saúde de um serviço
|
||||||
|
*/
|
||||||
|
getHealth(serviceId: string): HealthStatus {
|
||||||
|
return this.checks.get(serviceId)?.status || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna todos os status
|
||||||
|
*/
|
||||||
|
getAllHealth(): Map<string, HealthStatus> {
|
||||||
|
const result = new Map<string, HealthStatus>();
|
||||||
|
for (const [serviceId, check] of this.checks) {
|
||||||
|
result.set(serviceId, check.status);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Força um health check imediato
|
||||||
|
*/
|
||||||
|
async forceCheck(serviceId: string): Promise<HealthStatus> {
|
||||||
|
const check = this.checks.get(serviceId);
|
||||||
|
if (!check) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.performCheck(check);
|
||||||
|
return check.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa um health check
|
||||||
|
*/
|
||||||
|
private async performCheck(check: HealthCheckState): Promise<void> {
|
||||||
|
const { serviceId, config } = check;
|
||||||
|
const url = `http://localhost:${this.getServicePort(serviceId)}${config.path}`;
|
||||||
|
|
||||||
|
check.lastCheck = new Date();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), config.timeout || 5000);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.handleSuccess(check);
|
||||||
|
} else {
|
||||||
|
this.handleFailure(check, `HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
this.handleFailure(check, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trata sucesso do health check
|
||||||
|
*/
|
||||||
|
private handleSuccess(check: HealthCheckState): void {
|
||||||
|
check.consecutiveSuccesses++;
|
||||||
|
check.consecutiveFailures = 0;
|
||||||
|
check.lastError = undefined;
|
||||||
|
|
||||||
|
const requiredSuccesses = 2; // Necessário 2 sucessos consecutivos para ficar healthy
|
||||||
|
|
||||||
|
if (check.status !== 'healthy' && check.consecutiveSuccesses >= requiredSuccesses) {
|
||||||
|
const oldStatus = check.status;
|
||||||
|
check.status = 'healthy';
|
||||||
|
this.emitHealthChange(check, oldStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trata falha do health check
|
||||||
|
*/
|
||||||
|
private handleFailure(check: HealthCheckState, error: string): void {
|
||||||
|
check.consecutiveFailures++;
|
||||||
|
check.consecutiveSuccesses = 0;
|
||||||
|
check.lastError = error;
|
||||||
|
|
||||||
|
const maxFailures = check.config.retries || 3;
|
||||||
|
|
||||||
|
if (check.status !== 'unhealthy' && check.consecutiveFailures >= maxFailures) {
|
||||||
|
const oldStatus = check.status;
|
||||||
|
check.status = 'unhealthy';
|
||||||
|
this.emitHealthChange(check, oldStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emite mudança de saúde
|
||||||
|
*/
|
||||||
|
private emitHealthChange(check: HealthCheckState, oldStatus: HealthStatus): void {
|
||||||
|
this.emit('health:changed', {
|
||||||
|
serviceId: check.serviceId,
|
||||||
|
status: check.status,
|
||||||
|
oldStatus,
|
||||||
|
lastError: check.lastError,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.options.onHealthChange) {
|
||||||
|
this.options.onHealthChange(check.serviceId, check.status, check.lastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agenda próximo check
|
||||||
|
*/
|
||||||
|
private scheduleNextCheck(check: HealthCheckState): void {
|
||||||
|
if (check.timer) {
|
||||||
|
clearTimeout(check.timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
check.timer = setTimeout(() => {
|
||||||
|
this.performCheck(check).then(() => {
|
||||||
|
this.scheduleNextCheck(check);
|
||||||
|
});
|
||||||
|
}, check.config.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtém porta do serviço (placeholder - deve ser integrado com ProcessManager)
|
||||||
|
*/
|
||||||
|
private getServicePort(serviceId: string): number {
|
||||||
|
// TODO: Integrar com ProcessManager ou config
|
||||||
|
const ports: Record<string, number> = {
|
||||||
|
'python-contabil': 8003,
|
||||||
|
'python-bi': 8004,
|
||||||
|
'python-automation': 8005,
|
||||||
|
'node-communication': 9001,
|
||||||
|
'node-core-api': 9002,
|
||||||
|
'node-worker': 9003,
|
||||||
|
};
|
||||||
|
return ports[serviceId] || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
/**
|
||||||
|
* LogAggregator - Agregador de logs do Kernel
|
||||||
|
* Coleta, armazena e disponibiliza logs dos serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { LogEntry, ServiceConfig } from '../types';
|
||||||
|
|
||||||
|
export interface LogAggregatorOptions {
|
||||||
|
maxLines?: number;
|
||||||
|
retentionHours?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogAggregator extends EventEmitter {
|
||||||
|
private logs: LogEntry[] = [];
|
||||||
|
private serviceBuffers: Map<string, LogEntry[]> = new Map();
|
||||||
|
private options: Required<LogAggregatorOptions>;
|
||||||
|
|
||||||
|
constructor(options: LogAggregatorOptions = {}) {
|
||||||
|
super();
|
||||||
|
this.options = {
|
||||||
|
maxLines: 10000,
|
||||||
|
retentionHours: 24,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Limpa logs antigos periodicamente
|
||||||
|
setInterval(() => this.cleanup(), 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adiciona uma entrada de log
|
||||||
|
*/
|
||||||
|
add(entry: LogEntry): void {
|
||||||
|
// Adiciona ao buffer global
|
||||||
|
this.logs.push(entry);
|
||||||
|
|
||||||
|
// Adiciona ao buffer do serviço
|
||||||
|
if (!this.serviceBuffers.has(entry.serviceId)) {
|
||||||
|
this.serviceBuffers.set(entry.serviceId, []);
|
||||||
|
}
|
||||||
|
this.serviceBuffers.get(entry.serviceId)!.push(entry);
|
||||||
|
|
||||||
|
// Limita tamanho do buffer global
|
||||||
|
if (this.logs.length > this.options.maxLines) {
|
||||||
|
this.logs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limita tamanho do buffer por serviço
|
||||||
|
const serviceLogs = this.serviceBuffers.get(entry.serviceId)!;
|
||||||
|
if (serviceLogs.length > this.options.maxLines / 10) {
|
||||||
|
serviceLogs.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('log', entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adiciona log de sistema
|
||||||
|
*/
|
||||||
|
system(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
|
||||||
|
this.add({
|
||||||
|
timestamp: new Date(),
|
||||||
|
serviceId: 'kernel',
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
source: 'system',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna todos os logs
|
||||||
|
*/
|
||||||
|
getAll(options: {
|
||||||
|
lines?: number;
|
||||||
|
since?: Date;
|
||||||
|
level?: 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
serviceId?: string;
|
||||||
|
} = {}): LogEntry[] {
|
||||||
|
let logs = options.serviceId
|
||||||
|
? this.serviceBuffers.get(options.serviceId) || []
|
||||||
|
: [...this.logs];
|
||||||
|
|
||||||
|
if (options.since) {
|
||||||
|
logs = logs.filter(l => l.timestamp >= options.since!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.level) {
|
||||||
|
const levels = ['debug', 'info', 'warn', 'error'];
|
||||||
|
const minLevel = levels.indexOf(options.level);
|
||||||
|
logs = logs.filter(l => levels.indexOf(l.level) >= minLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.lines) {
|
||||||
|
logs = logs.slice(-options.lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna logs de um serviço específico
|
||||||
|
*/
|
||||||
|
getServiceLogs(serviceId: string, lines = 100): LogEntry[] {
|
||||||
|
const logs = this.serviceBuffers.get(serviceId) || [];
|
||||||
|
return logs.slice(-lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna logs recentes (últimos N)
|
||||||
|
*/
|
||||||
|
getRecent(count = 50): LogEntry[] {
|
||||||
|
return this.logs.slice(-count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna logs como string formatada
|
||||||
|
*/
|
||||||
|
formatLogs(logs: LogEntry[]): string {
|
||||||
|
return logs.map(log => this.formatEntry(log)).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formata uma entrada de log
|
||||||
|
*/
|
||||||
|
formatEntry(entry: LogEntry): string {
|
||||||
|
const time = entry.timestamp.toISOString().split('T')[1].slice(0, 12);
|
||||||
|
const level = entry.level.toUpperCase().padStart(5);
|
||||||
|
const service = entry.serviceId.padEnd(20);
|
||||||
|
return `[${time}] [${level}] [${service}] ${entry.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa logs de um serviço
|
||||||
|
*/
|
||||||
|
clearService(serviceId: string): void {
|
||||||
|
this.serviceBuffers.delete(serviceId);
|
||||||
|
this.logs = this.logs.filter(l => l.serviceId !== serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa todos os logs
|
||||||
|
*/
|
||||||
|
clearAll(): void {
|
||||||
|
this.logs = [];
|
||||||
|
this.serviceBuffers.clear();
|
||||||
|
this.system('Logs limpos', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna estatísticas de logs
|
||||||
|
*/
|
||||||
|
getStats(): {
|
||||||
|
totalLogs: number;
|
||||||
|
logsByService: Record<string, number>;
|
||||||
|
logsByLevel: Record<string, number>;
|
||||||
|
} {
|
||||||
|
const logsByService: Record<string, number> = {};
|
||||||
|
const logsByLevel: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const log of this.logs) {
|
||||||
|
logsByService[log.serviceId] = (logsByService[log.serviceId] || 0) + 1;
|
||||||
|
logsByLevel[log.level] = (logsByLevel[log.level] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalLogs: this.logs.length,
|
||||||
|
logsByService,
|
||||||
|
logsByLevel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream de logs em tempo real (retorna handler para unsubscribe)
|
||||||
|
*/
|
||||||
|
stream(callback: (entry: LogEntry) => void, options: {
|
||||||
|
serviceId?: string;
|
||||||
|
level?: 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
} = {}): () => void {
|
||||||
|
const handler = (entry: LogEntry) => {
|
||||||
|
if (options.serviceId && entry.serviceId !== options.serviceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.level) {
|
||||||
|
const levels = ['debug', 'info', 'warn', 'error'];
|
||||||
|
if (levels.indexOf(entry.level) < levels.indexOf(options.level)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(entry);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.on('log', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.off('log', handler);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limpa logs antigos
|
||||||
|
*/
|
||||||
|
private cleanup(): void {
|
||||||
|
const cutoff = new Date();
|
||||||
|
cutoff.setHours(cutoff.getHours() - this.options.retentionHours);
|
||||||
|
|
||||||
|
const initialCount = this.logs.length;
|
||||||
|
this.logs = this.logs.filter(l => l.timestamp >= cutoff);
|
||||||
|
|
||||||
|
for (const [serviceId, logs] of this.serviceBuffers) {
|
||||||
|
this.serviceBuffers.set(serviceId, logs.filter(l => l.timestamp >= cutoff));
|
||||||
|
}
|
||||||
|
|
||||||
|
const removed = initialCount - this.logs.length;
|
||||||
|
if (removed > 0) {
|
||||||
|
this.system(`Cleanup: ${removed} logs antigos removidos`, 'debug');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,346 @@
|
||||||
|
/**
|
||||||
|
* ProcessManager - Gerenciador de processos do Kernel
|
||||||
|
* Responsável por spawn, monitoramento e controle de serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
ServiceConfig,
|
||||||
|
ServiceState,
|
||||||
|
ServiceStatus,
|
||||||
|
HealthStatus,
|
||||||
|
LogEntry,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
export interface ProcessManagerOptions {
|
||||||
|
onLog?: (entry: LogEntry) => void;
|
||||||
|
onStateChange?: (serviceId: string, state: ServiceState) => void;
|
||||||
|
onHealthChange?: (serviceId: string, health: HealthStatus) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProcessManager extends EventEmitter {
|
||||||
|
private services: Map<string, ServiceState> = new Map();
|
||||||
|
private processes: Map<string, ChildProcess> = new Map();
|
||||||
|
private options: ProcessManagerOptions;
|
||||||
|
|
||||||
|
constructor(options: ProcessManagerOptions = {}) {
|
||||||
|
super();
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra um serviço no manager (sem iniciar)
|
||||||
|
*/
|
||||||
|
registerService(config: ServiceConfig): ServiceState {
|
||||||
|
const state: ServiceState = {
|
||||||
|
config,
|
||||||
|
status: 'stopped',
|
||||||
|
health: 'unknown',
|
||||||
|
restartCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.services.set(config.id, state);
|
||||||
|
this.emit('service:registered', { serviceId: config.id, state });
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia um serviço
|
||||||
|
*/
|
||||||
|
async startService(serviceId: string): Promise<ServiceState> {
|
||||||
|
const state = this.services.get(serviceId);
|
||||||
|
if (!state) {
|
||||||
|
throw new Error(`Serviço ${serviceId} não encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'running' || state.status === 'starting') {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.status = 'starting';
|
||||||
|
this.emitStateChange(serviceId, state);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const process = this.spawnProcess(state.config);
|
||||||
|
this.processes.set(serviceId, process);
|
||||||
|
|
||||||
|
state.pid = process.pid;
|
||||||
|
state.status = 'running';
|
||||||
|
state.startTime = new Date();
|
||||||
|
state.lastError = undefined;
|
||||||
|
|
||||||
|
this.setupProcessHandlers(serviceId, process);
|
||||||
|
this.emit('service:started', { serviceId, state });
|
||||||
|
|
||||||
|
return state;
|
||||||
|
} catch (error) {
|
||||||
|
state.status = 'error';
|
||||||
|
state.lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
this.emitStateChange(serviceId, state);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para um serviço
|
||||||
|
*/
|
||||||
|
async stopService(serviceId: string, force = false): Promise<ServiceState> {
|
||||||
|
const state = this.services.get(serviceId);
|
||||||
|
if (!state) {
|
||||||
|
throw new Error(`Serviço ${serviceId} não encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'stopped' || state.status === 'stopping') {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.status = 'stopping';
|
||||||
|
this.emitStateChange(serviceId, state);
|
||||||
|
|
||||||
|
const process = this.processes.get(serviceId);
|
||||||
|
if (process) {
|
||||||
|
if (force) {
|
||||||
|
process.kill('SIGKILL');
|
||||||
|
} else {
|
||||||
|
process.kill('SIGTERM');
|
||||||
|
|
||||||
|
// Aguarda 5s antes de forçar
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!process.killed) {
|
||||||
|
process.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reinicia um serviço
|
||||||
|
*/
|
||||||
|
async restartService(serviceId: string): Promise<ServiceState> {
|
||||||
|
const state = this.services.get(serviceId);
|
||||||
|
if (!state) {
|
||||||
|
throw new Error(`Serviço ${serviceId} não encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.status = 'restarting';
|
||||||
|
this.emitStateChange(serviceId, state);
|
||||||
|
|
||||||
|
await this.stopService(serviceId);
|
||||||
|
|
||||||
|
// Aguarda processo parar completamente
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
return this.startService(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mata um serviço (force kill)
|
||||||
|
*/
|
||||||
|
async killService(serviceId: string): Promise<ServiceState> {
|
||||||
|
return this.stopService(serviceId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia todos os serviços com autoStart
|
||||||
|
*/
|
||||||
|
async startAll(autoOnly = true): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (const [serviceId, state] of this.services) {
|
||||||
|
if (autoOnly && !state.config.autoStart) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
this.startService(serviceId)
|
||||||
|
.then(() => {
|
||||||
|
this.log('system', serviceId, 'info', `Serviço ${state.config.name} iniciado`);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.log('system', serviceId, 'error', `Falha ao iniciar ${state.config.name}: ${error.message}`);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para todos os serviços
|
||||||
|
*/
|
||||||
|
async stopAll(force = false): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (const [serviceId, state] of this.services) {
|
||||||
|
if (state.status === 'running' || state.status === 'starting') {
|
||||||
|
promises.push(
|
||||||
|
this.stopService(serviceId, force)
|
||||||
|
.catch(error => {
|
||||||
|
this.log('system', serviceId, 'error', `Erro ao parar serviço: ${error.message}`);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna estado de um serviço
|
||||||
|
*/
|
||||||
|
getServiceState(serviceId: string): ServiceState | undefined {
|
||||||
|
return this.services.get(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna todos os estados
|
||||||
|
*/
|
||||||
|
getAllStates(): ServiceState[] {
|
||||||
|
return Array.from(this.services.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna processo de um serviço
|
||||||
|
*/
|
||||||
|
getProcess(serviceId: string): ChildProcess | undefined {
|
||||||
|
return this.processes.get(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica se serviço está rodando
|
||||||
|
*/
|
||||||
|
isRunning(serviceId: string): boolean {
|
||||||
|
const state = this.services.get(serviceId);
|
||||||
|
return state?.status === 'running';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atualiza health status de um serviço
|
||||||
|
*/
|
||||||
|
setHealth(serviceId: string, health: HealthStatus): void {
|
||||||
|
const state = this.services.get(serviceId);
|
||||||
|
if (state && state.health !== health) {
|
||||||
|
state.health = health;
|
||||||
|
this.emit('service:health_changed', { serviceId, health });
|
||||||
|
|
||||||
|
if (this.options.onHealthChange) {
|
||||||
|
this.options.onHealthChange(serviceId, health);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawna um processo
|
||||||
|
*/
|
||||||
|
private spawnProcess(config: ServiceConfig): ChildProcess {
|
||||||
|
const cwd = config.cwd
|
||||||
|
? path.resolve(process.cwd(), config.cwd)
|
||||||
|
: process.cwd();
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...config.env,
|
||||||
|
};
|
||||||
|
|
||||||
|
const process_ = spawn(config.command, config.args, {
|
||||||
|
cwd,
|
||||||
|
env,
|
||||||
|
detached: false,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!process_.pid) {
|
||||||
|
throw new Error(`Falha ao spawnar processo ${config.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return process_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configura handlers de processo
|
||||||
|
*/
|
||||||
|
private setupProcessHandlers(serviceId: string, process: ChildProcess): void {
|
||||||
|
const state = this.services.get(serviceId)!;
|
||||||
|
|
||||||
|
// stdout
|
||||||
|
process.stdout?.on('data', (data: Buffer) => {
|
||||||
|
const message = data.toString().trim();
|
||||||
|
if (message) {
|
||||||
|
state.lastLog = message;
|
||||||
|
this.log('stdout', serviceId, 'info', message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// stderr
|
||||||
|
process.stderr?.on('data', (data: Buffer) => {
|
||||||
|
const message = data.toString().trim();
|
||||||
|
if (message) {
|
||||||
|
this.log('stderr', serviceId, 'error', message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// exit
|
||||||
|
process.on('exit', (code: number | null, signal: string | null) => {
|
||||||
|
this.processes.delete(serviceId);
|
||||||
|
|
||||||
|
const wasRunning = state.status === 'running';
|
||||||
|
state.status = 'stopped';
|
||||||
|
state.pid = undefined;
|
||||||
|
|
||||||
|
this.emitStateChange(serviceId, state);
|
||||||
|
this.emit('service:stopped', { serviceId, code, signal });
|
||||||
|
|
||||||
|
// Auto-restart se saiu com erro
|
||||||
|
if (wasRunning && code !== 0 && state.restartCount < (state.config.maxRestarts || 3)) {
|
||||||
|
state.restartCount++;
|
||||||
|
this.log('system', serviceId, 'warn', `Serviço parou (código ${code}). Reiniciando... (tentativa ${state.restartCount})`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.startService(serviceId).catch(() => {});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// error
|
||||||
|
process.on('error', (error: Error) => {
|
||||||
|
state.lastError = error.message;
|
||||||
|
this.log('system', serviceId, 'error', `Erro no processo: ${error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emite log
|
||||||
|
*/
|
||||||
|
private log(source: 'stdout' | 'stderr' | 'system', serviceId: string, level: 'info' | 'warn' | 'error' | 'debug', message: string): void {
|
||||||
|
const entry: LogEntry = {
|
||||||
|
timestamp: new Date(),
|
||||||
|
serviceId,
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit('log', entry);
|
||||||
|
|
||||||
|
if (this.options.onLog) {
|
||||||
|
this.options.onLog(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emite mudança de estado
|
||||||
|
*/
|
||||||
|
private emitStateChange(serviceId: string, state: ServiceState): void {
|
||||||
|
this.emit('service:state_changed', { serviceId, state });
|
||||||
|
|
||||||
|
if (this.options.onStateChange) {
|
||||||
|
this.options.onStateChange(serviceId, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
* DashboardServer - Servidor do Dashboard do Kernel
|
||||||
|
* Interface web na porta 5001 para gerenciamento dos serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express, { Application, Request, Response } from 'express';
|
||||||
|
import { createServer, Server } from 'http';
|
||||||
|
import { ProcessManager } from '../core/ProcessManager';
|
||||||
|
import { HealthMonitor } from '../core/HealthMonitor';
|
||||||
|
import { LogAggregator } from '../core/LogAggregator';
|
||||||
|
import { KernelWebSocket } from '../websocket/KernelWebSocket';
|
||||||
|
import { createKernelRoutes } from '../api/routes';
|
||||||
|
|
||||||
|
export interface DashboardServerOptions {
|
||||||
|
port?: number;
|
||||||
|
host?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DashboardServer {
|
||||||
|
private app: Application;
|
||||||
|
private server?: Server;
|
||||||
|
private webSocket?: KernelWebSocket;
|
||||||
|
private options: Required<DashboardServerOptions>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private processManager: ProcessManager,
|
||||||
|
private healthMonitor: HealthMonitor,
|
||||||
|
private logAggregator: LogAggregator,
|
||||||
|
options: DashboardServerOptions = {}
|
||||||
|
) {
|
||||||
|
this.options = {
|
||||||
|
port: 5001,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.app = express();
|
||||||
|
this.setupMiddleware();
|
||||||
|
this.setupRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.server = createServer(this.app);
|
||||||
|
|
||||||
|
this.webSocket = new KernelWebSocket(
|
||||||
|
this.processManager,
|
||||||
|
this.healthMonitor,
|
||||||
|
this.logAggregator
|
||||||
|
);
|
||||||
|
this.webSocket.attach(this.server);
|
||||||
|
|
||||||
|
this.server.listen(this.options.port, this.options.host, () => {
|
||||||
|
console.log(`[Kernel] Dashboard em http://${this.options.host}:${this.options.port}`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.webSocket?.stop();
|
||||||
|
this.server?.close(() => {
|
||||||
|
console.log('[Kernel] Dashboard parado');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning(): boolean {
|
||||||
|
return this.server?.listening || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupMiddleware(): void {
|
||||||
|
this.app.use(express.json());
|
||||||
|
|
||||||
|
this.app.use((req, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupRoutes(): void {
|
||||||
|
this.app.get('/health', (req: Request, res: Response) => {
|
||||||
|
res.json({ status: 'healthy', service: 'arcadia-kernel', timestamp: new Date() });
|
||||||
|
});
|
||||||
|
|
||||||
|
const kernelRoutes = createKernelRoutes(
|
||||||
|
this.processManager,
|
||||||
|
this.healthMonitor,
|
||||||
|
this.logAggregator
|
||||||
|
);
|
||||||
|
this.app.use('/api/kernel', kernelRoutes);
|
||||||
|
|
||||||
|
this.app.get('/', (req: Request, res: Response) => {
|
||||||
|
res.json({
|
||||||
|
name: 'Arcadia Kernel Dashboard',
|
||||||
|
version: '1.0.0',
|
||||||
|
endpoints: {
|
||||||
|
dashboard: '/',
|
||||||
|
health: '/health',
|
||||||
|
api: '/api/kernel',
|
||||||
|
websocket: '/ws/kernel',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* Arcadia Kernel - Entry Point
|
||||||
|
* Sistema nativo de gerenciamento de serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { ProcessManager } from './core/ProcessManager';
|
||||||
|
import { HealthMonitor } from './core/HealthMonitor';
|
||||||
|
import { LogAggregator } from './core/LogAggregator';
|
||||||
|
import { DashboardServer } from './dashboard/DashboardServer';
|
||||||
|
import { ServiceConfig, ServiceState } from './types';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
export interface KernelOptions {
|
||||||
|
configPath?: string;
|
||||||
|
dashboard?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
port?: number;
|
||||||
|
host?: string;
|
||||||
|
};
|
||||||
|
autoStart?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArcadiaKernel {
|
||||||
|
public processManager: ProcessManager;
|
||||||
|
public healthMonitor: HealthMonitor;
|
||||||
|
public logAggregator: LogAggregator;
|
||||||
|
public dashboard?: DashboardServer;
|
||||||
|
|
||||||
|
private options: Required<KernelOptions>;
|
||||||
|
private services: ServiceConfig[] = [];
|
||||||
|
private started = false;
|
||||||
|
|
||||||
|
constructor(options: KernelOptions = {}) {
|
||||||
|
this.options = {
|
||||||
|
configPath: join(__dirname, 'config', 'services.json'),
|
||||||
|
dashboard: {
|
||||||
|
enabled: true,
|
||||||
|
port: 5001,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
},
|
||||||
|
autoStart: false,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicializa componentes
|
||||||
|
this.logAggregator = new LogAggregator({ maxLines: 10000 });
|
||||||
|
|
||||||
|
this.processManager = new ProcessManager({
|
||||||
|
onLog: (entry) => this.logAggregator.add(entry),
|
||||||
|
onStateChange: (serviceId, state) => {
|
||||||
|
console.log(`[Kernel] ${serviceId}: ${state.status}`);
|
||||||
|
},
|
||||||
|
onHealthChange: (serviceId, health) => {
|
||||||
|
console.log(`[Kernel] ${serviceId} health: ${health}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.healthMonitor = new HealthMonitor({
|
||||||
|
onHealthChange: (serviceId, health, error) => {
|
||||||
|
this.processManager.setHealth(serviceId, health);
|
||||||
|
if (error) {
|
||||||
|
console.log(`[Kernel] ${serviceId} health error: ${error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Carrega configurações
|
||||||
|
this.loadConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia o Kernel
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Kernel] Iniciando Arcadia Kernel...');
|
||||||
|
console.log(`[Kernel] ${this.services.length} serviços registrados`);
|
||||||
|
|
||||||
|
// Registra serviços no ProcessManager
|
||||||
|
for (const config of this.services) {
|
||||||
|
this.processManager.registerService(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicia Dashboard
|
||||||
|
if (this.options.dashboard.enabled) {
|
||||||
|
this.dashboard = new DashboardServer(
|
||||||
|
this.processManager,
|
||||||
|
this.healthMonitor,
|
||||||
|
this.logAggregator,
|
||||||
|
{
|
||||||
|
port: this.options.dashboard.port,
|
||||||
|
host: this.options.dashboard.host,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await this.dashboard.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-start serviços
|
||||||
|
if (this.options.autoStart) {
|
||||||
|
console.log('[Kernel] Auto-iniciando serviços...');
|
||||||
|
await this.processManager.startAll(true);
|
||||||
|
|
||||||
|
// Inicia health monitoring para serviços em execução
|
||||||
|
for (const state of this.processManager.getAllStates()) {
|
||||||
|
if (state.status === 'running') {
|
||||||
|
this.healthMonitor.startMonitoring(state.config.id, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.started = true;
|
||||||
|
console.log('[Kernel] Kernel iniciado com sucesso!');
|
||||||
|
|
||||||
|
// Eventos de shutdown
|
||||||
|
this.setupShutdownHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para o Kernel
|
||||||
|
*/
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (!this.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Kernel] Parando Kernel...');
|
||||||
|
|
||||||
|
// Para health monitor
|
||||||
|
this.healthMonitor.stopAll();
|
||||||
|
|
||||||
|
// Para todos os serviços
|
||||||
|
await this.processManager.stopAll(true);
|
||||||
|
|
||||||
|
// Para dashboard
|
||||||
|
await this.dashboard?.stop();
|
||||||
|
|
||||||
|
this.started = false;
|
||||||
|
console.log('[Kernel] Kernel parado.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna se o kernel está rodando
|
||||||
|
*/
|
||||||
|
isRunning(): boolean {
|
||||||
|
return this.started;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Carrega configurações dos serviços
|
||||||
|
*/
|
||||||
|
private loadConfig(): void {
|
||||||
|
try {
|
||||||
|
const configData = readFileSync(this.options.configPath, 'utf-8');
|
||||||
|
const config = JSON.parse(configData);
|
||||||
|
this.services = config.services || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Kernel] Erro ao carregar config:', error);
|
||||||
|
this.services = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configura handlers de shutdown
|
||||||
|
*/
|
||||||
|
private setupShutdownHandlers(): void {
|
||||||
|
const shutdown = async (signal: string) => {
|
||||||
|
console.log(`\n[Kernel] Sinal ${signal} recebido. Desligando...`);
|
||||||
|
await this.stop();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('[Kernel] Uncaught Exception:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
console.error('[Kernel] Unhandled Rejection:', reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporta componentes individuais
|
||||||
|
export { ProcessManager } from './core/ProcessManager';
|
||||||
|
export { HealthMonitor } from './core/HealthMonitor';
|
||||||
|
export { LogAggregator } from './core/LogAggregator';
|
||||||
|
export { DashboardServer } from './dashboard/DashboardServer';
|
||||||
|
export { KernelWebSocket } from './websocket/KernelWebSocket';
|
||||||
|
export { createKernelRoutes } from './api/routes';
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
// Entry point para execução direta
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const kernel = new ArcadiaKernel({
|
||||||
|
dashboard: { enabled: true, port: 5001 },
|
||||||
|
autoStart: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
kernel.start().catch((error) => {
|
||||||
|
console.error('[Kernel] Falha ao iniciar:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
/**
|
||||||
|
* Tipos e Interfaces do Arcadia Kernel
|
||||||
|
* Sistema de gerenciamento nativo de serviços
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ServiceType = 'python' | 'node';
|
||||||
|
export type ServiceStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'error' | 'restarting';
|
||||||
|
export type HealthStatus = 'healthy' | 'unhealthy' | 'unknown' | 'checking';
|
||||||
|
|
||||||
|
export interface HealthCheckConfig {
|
||||||
|
path: string;
|
||||||
|
interval: number; // ms
|
||||||
|
timeout?: number; // ms
|
||||||
|
retries?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceConfig {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: ServiceType;
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
cwd?: string;
|
||||||
|
port: number;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
healthCheck?: HealthCheckConfig;
|
||||||
|
maxRestarts?: number;
|
||||||
|
autoStart?: boolean;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceState {
|
||||||
|
config: ServiceConfig;
|
||||||
|
status: ServiceStatus;
|
||||||
|
health: HealthStatus;
|
||||||
|
pid?: number;
|
||||||
|
startTime?: Date;
|
||||||
|
restartCount: number;
|
||||||
|
lastError?: string;
|
||||||
|
lastLog?: string;
|
||||||
|
cpu?: number; // CPU usage %
|
||||||
|
memory?: number; // Memory in MB
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
timestamp: Date;
|
||||||
|
serviceId: string;
|
||||||
|
level: 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
message: string;
|
||||||
|
source: 'stdout' | 'stderr' | 'system';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KernelStats {
|
||||||
|
totalServices: number;
|
||||||
|
runningServices: number;
|
||||||
|
healthyServices: number;
|
||||||
|
errorServices: number;
|
||||||
|
totalRestarts: number;
|
||||||
|
uptime: number; // ms since kernel start
|
||||||
|
cpuUsage: number;
|
||||||
|
memoryUsage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TenantStats {
|
||||||
|
tenantId: number;
|
||||||
|
name: string;
|
||||||
|
activeUsers: number;
|
||||||
|
totalRequests: number;
|
||||||
|
lastActivity: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardData {
|
||||||
|
services: ServiceState[];
|
||||||
|
stats: KernelStats;
|
||||||
|
tenants: TenantStats[];
|
||||||
|
recentLogs: LogEntry[];
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KernelConfig {
|
||||||
|
dashboard: {
|
||||||
|
port: number;
|
||||||
|
host: string;
|
||||||
|
authEnabled: boolean;
|
||||||
|
};
|
||||||
|
websocket: {
|
||||||
|
path: string;
|
||||||
|
heartbeatInterval: number;
|
||||||
|
};
|
||||||
|
healthCheck: {
|
||||||
|
defaultInterval: number;
|
||||||
|
defaultTimeout: number;
|
||||||
|
defaultRetries: number;
|
||||||
|
};
|
||||||
|
logging: {
|
||||||
|
maxLines: number;
|
||||||
|
retentionHours: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket event types
|
||||||
|
export type KernelEventType =
|
||||||
|
| 'service:started'
|
||||||
|
| 'service:stopped'
|
||||||
|
| 'service:restarted'
|
||||||
|
| 'service:error'
|
||||||
|
| 'service:health_changed'
|
||||||
|
| 'log:new'
|
||||||
|
| 'stats:update'
|
||||||
|
| 'tenant:activity';
|
||||||
|
|
||||||
|
export interface KernelEvent {
|
||||||
|
type: KernelEventType;
|
||||||
|
timestamp: Date;
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Response types
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
timestamp: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceActionRequest {
|
||||||
|
action: 'start' | 'stop' | 'restart' | 'kill';
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceLogsRequest {
|
||||||
|
lines?: number;
|
||||||
|
since?: Date;
|
||||||
|
level?: 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
/**
|
||||||
|
* KernelWebSocket - WebSocket para atualizações em tempo real
|
||||||
|
* Transmite logs, status e eventos do kernel
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
|
import { Server } from 'http';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { ProcessManager } from '../core/ProcessManager';
|
||||||
|
import { HealthMonitor } from '../core/HealthMonitor';
|
||||||
|
import { LogAggregator } from '../core/LogAggregator';
|
||||||
|
import { LogEntry, ServiceState, KernelEvent, HealthStatus } from '../types';
|
||||||
|
|
||||||
|
interface ClientConnection {
|
||||||
|
ws: WebSocket;
|
||||||
|
id: string;
|
||||||
|
subscribedServices: Set<string>;
|
||||||
|
subscribedEvents: Set<string>;
|
||||||
|
isAlive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KernelWebSocketOptions {
|
||||||
|
path?: string;
|
||||||
|
heartbeatInterval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KernelWebSocket extends EventEmitter {
|
||||||
|
private wss?: WebSocketServer;
|
||||||
|
private clients: Map<string, ClientConnection> = new Map();
|
||||||
|
private options: Required<KernelWebSocketOptions>;
|
||||||
|
private heartbeatTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private processManager: ProcessManager,
|
||||||
|
private healthMonitor: HealthMonitor,
|
||||||
|
private logAggregator: LogAggregator,
|
||||||
|
options: KernelWebSocketOptions = {}
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.options = {
|
||||||
|
path: '/ws/kernel',
|
||||||
|
heartbeatInterval: 30000,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia o WebSocket server
|
||||||
|
*/
|
||||||
|
attach(server: Server): void {
|
||||||
|
this.wss = new WebSocketServer({
|
||||||
|
server,
|
||||||
|
path: this.options.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wss.on('connection', (ws, req) => {
|
||||||
|
this.handleConnection(ws, req);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicia heartbeat
|
||||||
|
this.startHeartbeat();
|
||||||
|
|
||||||
|
// Subscribe aos eventos do kernel
|
||||||
|
this.subscribeToKernelEvents();
|
||||||
|
|
||||||
|
console.log(`[Kernel] WebSocket ativo em ${this.options.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para o WebSocket server
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (this.heartbeatTimer) {
|
||||||
|
clearInterval(this.heartbeatTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fecha todas as conexões
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
client.ws.close();
|
||||||
|
}
|
||||||
|
this.clients.clear();
|
||||||
|
|
||||||
|
this.wss?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast para todos os clientes
|
||||||
|
*/
|
||||||
|
broadcast(event: string, data: any): void {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: event,
|
||||||
|
data,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
if (client.ws.readyState === WebSocket.OPEN) {
|
||||||
|
client.ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast para clientes inscritos em um serviço
|
||||||
|
*/
|
||||||
|
broadcastToService(serviceId: string, event: string, data: any): void {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: event,
|
||||||
|
serviceId,
|
||||||
|
data,
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
if (
|
||||||
|
client.ws.readyState === WebSocket.OPEN &&
|
||||||
|
(client.subscribedServices.has(serviceId) || client.subscribedServices.has('*'))
|
||||||
|
) {
|
||||||
|
client.ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna número de clientes conectados
|
||||||
|
*/
|
||||||
|
getClientCount(): number {
|
||||||
|
return this.clients.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler de nova conexão
|
||||||
|
*/
|
||||||
|
private handleConnection(ws: WebSocket, req: any): void {
|
||||||
|
const clientId = this.generateClientId();
|
||||||
|
const client: ClientConnection = {
|
||||||
|
ws,
|
||||||
|
id: clientId,
|
||||||
|
subscribedServices: new Set(),
|
||||||
|
subscribedEvents: new Set(),
|
||||||
|
isAlive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.clients.set(clientId, client);
|
||||||
|
console.log(`[Kernel] Cliente WebSocket conectado: ${clientId}`);
|
||||||
|
|
||||||
|
// Envia estado inicial
|
||||||
|
this.sendInitialState(client);
|
||||||
|
|
||||||
|
// Handlers de mensagens
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
this.handleMessage(client, data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Heartbeat
|
||||||
|
ws.on('pong', () => {
|
||||||
|
client.isAlive = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup na desconexão
|
||||||
|
ws.on('close', () => {
|
||||||
|
this.clients.delete(clientId);
|
||||||
|
console.log(`[Kernel] Cliente WebSocket desconectado: ${clientId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Erro
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.error(`[Kernel] Erro WebSocket ${clientId}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler de mensagens do cliente
|
||||||
|
*/
|
||||||
|
private handleMessage(client: ClientConnection, message: string): void {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
const { action, payload } = data;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'subscribe':
|
||||||
|
// Inscreve em serviços específicos ou '*' para todos
|
||||||
|
if (payload.services) {
|
||||||
|
for (const serviceId of payload.services) {
|
||||||
|
client.subscribedServices.add(serviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.events) {
|
||||||
|
for (const event of payload.events) {
|
||||||
|
client.subscribedEvents.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.send(client, 'subscribed', {
|
||||||
|
services: Array.from(client.subscribedServices),
|
||||||
|
events: Array.from(client.subscribedEvents),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'unsubscribe':
|
||||||
|
if (payload.services) {
|
||||||
|
for (const serviceId of payload.services) {
|
||||||
|
client.subscribedServices.delete(serviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (payload.events) {
|
||||||
|
for (const event of payload.events) {
|
||||||
|
client.subscribedEvents.delete(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'getLogs':
|
||||||
|
// Retorna logs de um serviço
|
||||||
|
const logs = this.logAggregator.getServiceLogs(
|
||||||
|
payload.serviceId,
|
||||||
|
payload.lines || 50
|
||||||
|
);
|
||||||
|
this.send(client, 'logs', {
|
||||||
|
serviceId: payload.serviceId,
|
||||||
|
logs,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ping':
|
||||||
|
this.send(client, 'pong', { timestamp: new Date() });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this.send(client, 'error', { message: `Ação desconhecida: ${action}` });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.send(client, 'error', {
|
||||||
|
message: error instanceof Error ? error.message : 'Erro ao processar mensagem'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia mensagem para um cliente
|
||||||
|
*/
|
||||||
|
private send(client: ClientConnection, type: string, data: any): void {
|
||||||
|
if (client.ws.readyState === WebSocket.OPEN) {
|
||||||
|
client.ws.send(JSON.stringify({
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
timestamp: new Date(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia estado inicial para novo cliente
|
||||||
|
*/
|
||||||
|
private sendInitialState(client: ClientConnection): void {
|
||||||
|
const states = this.processManager.getAllStates();
|
||||||
|
const allHealth = this.healthMonitor.getAllHealth();
|
||||||
|
|
||||||
|
this.send(client, 'initial', {
|
||||||
|
services: states.map(s => ({
|
||||||
|
id: s.config.id,
|
||||||
|
name: s.config.name,
|
||||||
|
status: s.status,
|
||||||
|
health: allHealth.get(s.config.id) || 'unknown',
|
||||||
|
pid: s.pid,
|
||||||
|
startTime: s.startTime,
|
||||||
|
})),
|
||||||
|
timestamp: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inscreve nos eventos do kernel
|
||||||
|
*/
|
||||||
|
private subscribeToKernelEvents(): void {
|
||||||
|
// Eventos de serviço
|
||||||
|
this.processManager.on('service:started', ({ serviceId, state }) => {
|
||||||
|
this.broadcastToService(serviceId, 'service:started', {
|
||||||
|
serviceId,
|
||||||
|
state: {
|
||||||
|
id: state.config.id,
|
||||||
|
name: state.config.name,
|
||||||
|
status: state.status,
|
||||||
|
pid: state.pid,
|
||||||
|
startTime: state.startTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.processManager.on('service:stopped', ({ serviceId, code, signal }) => {
|
||||||
|
this.broadcastToService(serviceId, 'service:stopped', {
|
||||||
|
serviceId,
|
||||||
|
code,
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.processManager.on('service:state_changed', ({ serviceId, state }) => {
|
||||||
|
this.broadcastToService(serviceId, 'service:state_changed', {
|
||||||
|
serviceId,
|
||||||
|
status: state.status,
|
||||||
|
restartCount: state.restartCount,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eventos de health
|
||||||
|
this.healthMonitor.on('health:changed', ({ serviceId, status, oldStatus }) => {
|
||||||
|
this.broadcastToService(serviceId, 'service:health_changed', {
|
||||||
|
serviceId,
|
||||||
|
health: status,
|
||||||
|
previousHealth: oldStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Eventos de log
|
||||||
|
this.logAggregator.on('log', (entry: LogEntry) => {
|
||||||
|
this.broadcastToService(entry.serviceId, 'log:new', entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia heartbeat para detectar conexões mortas
|
||||||
|
*/
|
||||||
|
private startHeartbeat(): void {
|
||||||
|
this.heartbeatTimer = setInterval(() => {
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
if (!client.isAlive) {
|
||||||
|
client.ws.terminate();
|
||||||
|
this.clients.delete(client.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.isAlive = false;
|
||||||
|
client.ws.ping();
|
||||||
|
}
|
||||||
|
}, this.options.heartbeatInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gera ID único para cliente
|
||||||
|
*/
|
||||||
|
private generateClientId(): string {
|
||||||
|
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue