feat(kernel): integração Engine Room com Arcadia Kernel
MIGRAÇÃO COMPLETA: Sistema antigo → Kernel Nativo (porta 5001) 📁 Arquivos alterados: - server/engine-room/kernel-adapter.ts (NOVO) - server/engine-room/routes.ts - server/kernel/config/services.json 🔧 O que mudou no Kernel (services.json): + Adicionado: python-fisco (porta 8002) + Corrigido: node-communication (8006, era 9001) + Adicionado: node-core-api (9002) + Adicionado: node-worker (9003) - Removido: java-metaset (gerenciado externamente) Total: 6 → 7 serviços gerenciados pelo Kernel 🔄 Engine Room - Novo comportamento: - Todas as rotas /api/engine-room/* agora usam Kernel Adapter - Sistema antigo (managedServices) mantido em comentário para fallback - Verificação automática se Kernel está disponível - Retorna erro 503 se Kernel não responde na porta 5001 📊 Mapeamento de serviços: Engine Room → Kernel ID ──────────────────────────────────────── contabil → python-contabil bi-engine → python-bi automation-engine → python-automation fisco → python-fisco communication → node-communication 🚫 Serviços externos (não gerenciados pelo Kernel): - plus (PHP/Laravel, porta 8080) - metaset (Java, porta 8088) ✨ Benefícios: - Auto-restart configurável por serviço - Health checks automáticos - Logs centralizados com timestamps - WebSocket para atualizações em tempo real - Dashboard na porta 5001 ⚠️ Pré-requisito: KERNEL_ENABLED=true npm run dev Refs: Semana 4 - Integração Kernel com Engine Room Breaking Change: Engine Room requer Kernel rodando
This commit is contained in:
parent
7ce8735c04
commit
2e76496ff4
|
|
@ -0,0 +1,268 @@
|
|||
/**
|
||||
* Kernel Adapter - Ponte entre Engine Room e Arcadia Kernel
|
||||
* Traduz chamadas do formato antigo para o novo Kernel (porta 5001)
|
||||
*/
|
||||
|
||||
const KERNEL_BASE_URL = 'http://localhost:5001/api/kernel';
|
||||
|
||||
// Mapeamento: Nome do Engine Room -> ID do Kernel
|
||||
// NOTA: metaset NÃO está aqui - é gerenciado externamente (Java)
|
||||
const ENGINE_TO_KERNEL_ID: Record<string, string> = {
|
||||
"contabil": "python-contabil",
|
||||
"bi-engine": "python-bi",
|
||||
"automation-engine": "python-automation",
|
||||
"fisco": "python-fisco",
|
||||
"communication": "node-communication",
|
||||
};
|
||||
|
||||
// Mapeamento inverso
|
||||
const KERNEL_TO_ENGINE_NAME: Record<string, string> = {
|
||||
"python-contabil": "contabil",
|
||||
"python-bi": "bi-engine",
|
||||
"python-automation": "automation-engine",
|
||||
"python-fisco": "fisco",
|
||||
"node-communication": "communication",
|
||||
};
|
||||
|
||||
// Mapeamento de categorias
|
||||
const CATEGORY_MAP: Record<string, string> = {
|
||||
"python-contabil": "fiscal",
|
||||
"python-bi": "data",
|
||||
"python-automation": "automation",
|
||||
"python-fisco": "fiscal",
|
||||
"node-communication": "intelligence",
|
||||
};
|
||||
|
||||
// Mapeamento de display names
|
||||
const DISPLAY_NAME_MAP: Record<string, string> = {
|
||||
"python-contabil": "Motor Contabil",
|
||||
"python-bi": "Motor BI",
|
||||
"python-automation": "Motor Automacao",
|
||||
"python-fisco": "Motor Fiscal",
|
||||
"node-communication": "Motor Comunicacao",
|
||||
};
|
||||
|
||||
interface KernelServiceState {
|
||||
config: {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
port: number;
|
||||
autoStart: boolean;
|
||||
maxRestarts: number;
|
||||
};
|
||||
status: 'running' | 'stopped' | 'error' | 'restarting';
|
||||
pid?: number;
|
||||
startTime?: string;
|
||||
restartCount: number;
|
||||
health?: 'healthy' | 'unhealthy' | 'unknown';
|
||||
}
|
||||
|
||||
interface EngineStatus {
|
||||
name: string;
|
||||
displayName: string;
|
||||
type: string;
|
||||
port: number;
|
||||
category: string;
|
||||
description: string;
|
||||
status: "online" | "offline" | "error";
|
||||
responseTime?: number;
|
||||
details?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Helper para fazer requests ao Kernel
|
||||
async function kernelRequest(endpoint: string, method = 'GET', body?: any): Promise<any> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${KERNEL_BASE_URL}${endpoint}`, options);
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Kernel responded with ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Kernel request timeout');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Converter status do Kernel para formato do Engine Room
|
||||
function mapStatus(kernelStatus: string, health?: string): "online" | "offline" | "error" {
|
||||
if (kernelStatus === 'running') {
|
||||
return health === 'unhealthy' ? 'error' : 'online';
|
||||
}
|
||||
if (kernelStatus === 'error') return 'error';
|
||||
return 'offline';
|
||||
}
|
||||
|
||||
// Obter todos os serviços do Kernel formatados para Engine Room
|
||||
export async function getKernelEngines(): Promise<EngineStatus[]> {
|
||||
try {
|
||||
const response = await kernelRequest('/services');
|
||||
const services: KernelServiceState[] = response.data || [];
|
||||
|
||||
return services.map((service): EngineStatus => {
|
||||
const engineName = KERNEL_TO_ENGINE_NAME[service.config.id] || service.config.id;
|
||||
|
||||
return {
|
||||
name: engineName,
|
||||
displayName: DISPLAY_NAME_MAP[service.config.id] || service.config.name,
|
||||
type: service.config.type === 'python' ? 'python' :
|
||||
service.config.type === 'java' ? 'java' : 'node',
|
||||
port: service.config.port,
|
||||
category: CATEGORY_MAP[service.config.id] || 'data',
|
||||
description: service.config.name,
|
||||
status: mapStatus(service.status, service.health),
|
||||
details: {
|
||||
pid: service.pid,
|
||||
restartCount: service.restartCount,
|
||||
startTime: service.startTime,
|
||||
health: service.health,
|
||||
},
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[KernelAdapter] Error fetching engines:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Obter info de um serviço específico (formato compatível com antigo)
|
||||
export async function getKernelServiceInfo(engineName: string): Promise<any> {
|
||||
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||
if (!kernelId) return null;
|
||||
|
||||
try {
|
||||
const response = await kernelRequest(`/services/${kernelId}`);
|
||||
const service: KernelServiceState = response.data;
|
||||
|
||||
if (!service) return null;
|
||||
|
||||
return {
|
||||
name: engineName,
|
||||
port: service.config.port,
|
||||
status: service.status,
|
||||
startedAt: service.startTime || null,
|
||||
restartCount: service.restartCount,
|
||||
uptime: service.startTime
|
||||
? Math.round((Date.now() - new Date(service.startTime).getTime()) / 1000)
|
||||
: 0,
|
||||
logLines: 0, // Kernel não expõe isso diretamente
|
||||
health: service.health,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`[KernelAdapter] Error getting info for ${engineName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Obter logs de um serviço (formato compatível)
|
||||
export async function getKernelServiceLogs(engineName: string, lines = 50): Promise<string[]> {
|
||||
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||
if (!kernelId) return [];
|
||||
|
||||
try {
|
||||
const response = await kernelRequest(`/services/${kernelId}/logs?lines=${lines}`);
|
||||
const logs = response.data || [];
|
||||
|
||||
// Converter formato do Kernel (objetos) para formato antigo (strings)
|
||||
return logs.map((log: any) => {
|
||||
const time = new Date(log.timestamp).toISOString().split('T')[1].slice(0, 8);
|
||||
return `[${time}] [${log.level.toUpperCase()}] ${log.message}`;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[KernelAdapter] Error getting logs for ${engineName}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar serviço
|
||||
export async function startKernelService(engineName: string): Promise<boolean> {
|
||||
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||
if (!kernelId) return false;
|
||||
|
||||
try {
|
||||
await kernelRequest(`/services/${kernelId}/start`, 'POST');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[KernelAdapter] Error starting ${engineName}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Parar serviço
|
||||
export async function stopKernelService(engineName: string): Promise<boolean> {
|
||||
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||
if (!kernelId) return false;
|
||||
|
||||
try {
|
||||
await kernelRequest(`/services/${kernelId}/stop`, 'POST');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[KernelAdapter] Error stopping ${engineName}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reiniciar serviço
|
||||
export async function restartKernelService(engineName: string): Promise<boolean> {
|
||||
const kernelId = ENGINE_TO_KERNEL_ID[engineName];
|
||||
if (!kernelId) return false;
|
||||
|
||||
try {
|
||||
await kernelRequest(`/services/${kernelId}/restart`, 'POST');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[KernelAdapter] Error restarting ${engineName}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar health geral do Kernel
|
||||
export async function getKernelHealth(): Promise<any> {
|
||||
try {
|
||||
const response = await kernelRequest('/health');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('[KernelAdapter] Error getting health:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar se Kernel está disponível
|
||||
export async function isKernelAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
const response = await fetch('http://localhost:5001/health', {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[KernelAdapter] Adapter loaded - proxying to localhost:5001');
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
import type { Express, Request, Response } from "express";
|
||||
import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index";
|
||||
import {
|
||||
getKernelEngines,
|
||||
getKernelServiceInfo,
|
||||
getKernelServiceLogs,
|
||||
startKernelService,
|
||||
stopKernelService,
|
||||
restartKernelService,
|
||||
getKernelHealth,
|
||||
isKernelAvailable
|
||||
} from "./kernel-adapter";
|
||||
|
||||
// Fallback para sistema antigo (import comentado - remover após validação)
|
||||
// import { restartManagedService, stopManagedService, getManagedServiceInfo, getManagedServiceLogs } from "../index";
|
||||
|
||||
interface EngineConfig {
|
||||
name: string;
|
||||
|
|
@ -120,7 +132,45 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const results = await Promise.all(ENGINES.map(checkEngineHealth));
|
||||
// Verificar se Kernel está disponível
|
||||
const kernelAvailable = await isKernelAvailable();
|
||||
if (!kernelAvailable) {
|
||||
return res.status(503).json({
|
||||
error: "Kernel não disponível",
|
||||
message: "O Arcadia Kernel (porta 5001) não está respondendo. Verifique se está rodando com KERNEL_ENABLED=true"
|
||||
});
|
||||
}
|
||||
|
||||
// Obter engines do Kernel
|
||||
const kernelEngines = await getKernelEngines();
|
||||
|
||||
// Adicionar Plus (gerenciado externamente - PHP/Laravel)
|
||||
const plusEngine = ENGINES.find(e => e.name === "plus");
|
||||
if (plusEngine) {
|
||||
const plusHealth = await checkEngineHealth(plusEngine);
|
||||
kernelEngines.push({
|
||||
...plusHealth,
|
||||
name: "plus",
|
||||
displayName: "Arcadia Plus (ERP)",
|
||||
type: "php",
|
||||
category: "erp",
|
||||
description: "ERP completo Laravel",
|
||||
} as any);
|
||||
}
|
||||
|
||||
// Adicionar MetaSet (gerenciado externamente - Java)
|
||||
const metasetEngine = ENGINES.find(e => e.name === "metaset");
|
||||
if (metasetEngine) {
|
||||
const metasetHealth = await checkEngineHealth(metasetEngine);
|
||||
kernelEngines.push({
|
||||
...metasetHealth,
|
||||
name: "metaset",
|
||||
displayName: "MetaSet (Motor BI)",
|
||||
type: "java",
|
||||
category: "data",
|
||||
description: "Motor de BI - Consultas, Dashboards, Gráficos",
|
||||
} as any);
|
||||
}
|
||||
|
||||
let agentsStatus: any[] = [];
|
||||
try {
|
||||
|
|
@ -130,11 +180,11 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
agentsStatus = [];
|
||||
}
|
||||
|
||||
const online = results.filter((r) => r.status === "online").length;
|
||||
const total = results.length;
|
||||
const online = kernelEngines.filter((r) => r.status === "online").length;
|
||||
const total = kernelEngines.length;
|
||||
|
||||
res.json({
|
||||
engines: results,
|
||||
engines: kernelEngines,
|
||||
agents: agentsStatus,
|
||||
summary: {
|
||||
total_engines: total,
|
||||
|
|
@ -239,13 +289,6 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
});
|
||||
|
||||
const ENGINE_NAME_MAP: Record<string, string> = {
|
||||
"contabil": "contabil",
|
||||
"fisco": "fisco",
|
||||
"bi-engine": "bi",
|
||||
"automation-engine": "automation",
|
||||
"metaset": "metaset",
|
||||
};
|
||||
|
||||
app.post("/api/engine-room/engine/:name/restart", async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
|
@ -254,17 +297,21 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
|
||||
const engineName = req.params.name;
|
||||
const serviceName = ENGINE_NAME_MAP[engineName];
|
||||
|
||||
if (engineName === "plus") {
|
||||
return res.status(400).json({ error: "Plus (Laravel) nao pode ser reiniciado por aqui" });
|
||||
}
|
||||
|
||||
if (!serviceName) {
|
||||
return res.status(404).json({ error: "Motor nao encontrado" });
|
||||
if (engineName === "metaset") {
|
||||
return res.status(400).json({ error: "MetaSet (Java) nao pode ser reiniciado por aqui" });
|
||||
}
|
||||
|
||||
const restarted = restartManagedService(serviceName);
|
||||
// Verificar se Kernel está disponível
|
||||
const kernelAvailable = await isKernelAvailable();
|
||||
if (!kernelAvailable) {
|
||||
return res.status(503).json({ error: "Kernel não disponível" });
|
||||
}
|
||||
|
||||
const restarted = await restartKernelService(engineName);
|
||||
if (restarted) {
|
||||
res.json({ success: true, message: `Motor ${engineName} reiniciando...` });
|
||||
} else {
|
||||
|
|
@ -282,17 +329,21 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
|
||||
const engineName = req.params.name;
|
||||
const serviceName = ENGINE_NAME_MAP[engineName];
|
||||
|
||||
if (engineName === "plus") {
|
||||
return res.status(400).json({ error: "Plus (Laravel) nao pode ser parado por aqui" });
|
||||
}
|
||||
|
||||
if (!serviceName) {
|
||||
return res.status(404).json({ error: "Motor nao encontrado" });
|
||||
if (engineName === "metaset") {
|
||||
return res.status(400).json({ error: "MetaSet (Java) nao pode ser parado por aqui" });
|
||||
}
|
||||
|
||||
const stopped = stopManagedService(serviceName);
|
||||
// Verificar se Kernel está disponível
|
||||
const kernelAvailable = await isKernelAvailable();
|
||||
if (!kernelAvailable) {
|
||||
return res.status(503).json({ error: "Kernel não disponível" });
|
||||
}
|
||||
|
||||
const stopped = await stopKernelService(engineName);
|
||||
if (stopped) {
|
||||
res.json({ success: true, message: `Motor ${engineName} parado` });
|
||||
} else {
|
||||
|
|
@ -310,18 +361,22 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
|
||||
const engineName = req.params.name;
|
||||
const serviceName = ENGINE_NAME_MAP[engineName];
|
||||
|
||||
if (engineName === "plus") {
|
||||
return res.status(400).json({ error: "Plus (Laravel) nao pode ser iniciado por aqui" });
|
||||
}
|
||||
|
||||
if (!serviceName) {
|
||||
return res.status(404).json({ error: "Motor nao encontrado" });
|
||||
if (engineName === "metaset") {
|
||||
return res.status(400).json({ error: "MetaSet (Java) nao pode ser iniciado por aqui" });
|
||||
}
|
||||
|
||||
const restarted = restartManagedService(serviceName);
|
||||
if (restarted) {
|
||||
// Verificar se Kernel está disponível
|
||||
const kernelAvailable = await isKernelAvailable();
|
||||
if (!kernelAvailable) {
|
||||
return res.status(503).json({ error: "Kernel não disponível" });
|
||||
}
|
||||
|
||||
const started = await startKernelService(engineName);
|
||||
if (started) {
|
||||
res.json({ success: true, message: `Motor ${engineName} iniciando...` });
|
||||
} else {
|
||||
res.status(500).json({ error: `Falha ao iniciar motor ${engineName}` });
|
||||
|
|
@ -338,16 +393,15 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
|
||||
const engineName = req.params.name;
|
||||
const serviceName = ENGINE_NAME_MAP[engineName];
|
||||
|
||||
if (!serviceName) {
|
||||
if (engineName === "plus") {
|
||||
return res.json({ name: "plus", port: 8080, status: "managed-externally", message: "Plus e gerenciado separadamente" });
|
||||
}
|
||||
return res.status(404).json({ error: "Motor nao encontrado" });
|
||||
if (engineName === "plus") {
|
||||
return res.json({ name: "plus", port: 8080, status: "managed-externally", message: "Plus e gerenciado separadamente" });
|
||||
}
|
||||
if (engineName === "metaset") {
|
||||
return res.json({ name: "metaset", port: 8088, status: "managed-externally", message: "MetaSet e gerenciado separadamente (Java)" });
|
||||
}
|
||||
|
||||
const info = getManagedServiceInfo(serviceName);
|
||||
const info = await getKernelServiceInfo(engineName);
|
||||
if (!info) {
|
||||
return res.status(404).json({ error: "Servico nao encontrado no gerenciador" });
|
||||
}
|
||||
|
|
@ -365,14 +419,14 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
|
||||
const engineName = req.params.name;
|
||||
const serviceName = ENGINE_NAME_MAP[engineName];
|
||||
|
||||
if (!serviceName) {
|
||||
return res.status(404).json({ error: "Motor nao encontrado" });
|
||||
// Serviços externos não têm logs via Kernel
|
||||
if (engineName === "plus" || engineName === "metaset") {
|
||||
return res.json({ engine: engineName, lines: 0, logs: [], message: "Logs nao disponiveis para servicos gerenciados externamente" });
|
||||
}
|
||||
|
||||
const lines = parseInt(req.query.lines as string) || 50;
|
||||
const logs = getManagedServiceLogs(serviceName, lines);
|
||||
const logs = await getKernelServiceLogs(engineName, lines);
|
||||
|
||||
res.json({ engine: engineName, lines: logs.length, logs });
|
||||
} catch (error: any) {
|
||||
|
|
@ -380,5 +434,5 @@ export function registerEngineRoomRoutes(app: Express): void {
|
|||
}
|
||||
});
|
||||
|
||||
console.log("[Engine Room] Rotas registradas em /api/engine-room/*");
|
||||
console.log("[Engine Room] Rotas registradas em /api/engine-room/* (usando Kernel Adapter)");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,29 @@
|
|||
"maxRestarts": 5,
|
||||
"autoStart": true
|
||||
},
|
||||
{
|
||||
"id": "python-fisco",
|
||||
"name": "Motor Fiscal",
|
||||
"description": "NF-e/NFC-e - NCMs, CFOPs, CESTs, SEFAZ",
|
||||
"type": "python",
|
||||
"command": "python3",
|
||||
"args": ["-m", "uvicorn", "fisco_service:app", "--host", "0.0.0.0", "--port", "8002"],
|
||||
"cwd": "./server/python",
|
||||
"port": 8002,
|
||||
"healthCheck": {
|
||||
"path": "/health",
|
||||
"interval": 30000,
|
||||
"timeout": 5000,
|
||||
"retries": 3
|
||||
},
|
||||
"env": {
|
||||
"PYTHONPATH": ".",
|
||||
"SERVICE_NAME": "fisco",
|
||||
"LOG_LEVEL": "info"
|
||||
},
|
||||
"maxRestarts": 5,
|
||||
"autoStart": false
|
||||
},
|
||||
{
|
||||
"id": "node-communication",
|
||||
"name": "Communication Service",
|
||||
|
|
@ -76,7 +99,7 @@
|
|||
"type": "node",
|
||||
"command": "node",
|
||||
"args": ["dist/services/communication/index.js"],
|
||||
"port": 9001,
|
||||
"port": 8006,
|
||||
"healthCheck": {
|
||||
"path": "/health",
|
||||
"interval": 30000,
|
||||
|
|
@ -86,11 +109,12 @@
|
|||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"SERVICE_NAME": "communication",
|
||||
"PORT": "9001"
|
||||
"PORT": "8006"
|
||||
},
|
||||
"maxRestarts": 5,
|
||||
"autoStart": false
|
||||
},
|
||||
|
||||
{
|
||||
"id": "node-core-api",
|
||||
"name": "Core API Service",
|
||||
|
|
|
|||
Loading…
Reference in New Issue