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 path from "path";
|
||||
import { logger, httpLogger } from "./logger";
|
||||
import { ArcadiaKernel } from "./kernel"; // Kernel Nativo - Semana 3
|
||||
|
||||
interface ManagedService {
|
||||
name: string;
|
||||
|
|
@ -397,4 +398,21 @@ export function log(message: string, source = "express") {
|
|||
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