feat(kernel): implementa Service Discovery e Registry (Semana 4)
Adiciona módulos de Service Discovery ao Kernel Arcádia: - ServiceRegistry: catálogo central de serviços com health check - DockerDiscovery: descoberta automática via labels Docker - API REST /api/registry/* para consulta de serviços - Labels arcadia.* no docker-compose.yml para auto-registro - Integração não-destrutiva (módulos novos, código antigo preservado) Labels suportadas: - arcadia.discovery.enabled=true - arcadia.name, arcadia.type, arcadia.category - arcadia.port, arcadia.capabilities - arcadia.requires_ia, arcadia.ia_policies Arquitetura: MODIFICAR (adicionar) ao invés de REFATORAR
This commit is contained in:
parent
2916fb04ae
commit
7651167d72
|
|
@ -83,6 +83,16 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "arcadia.discovery.enabled=true"
|
||||||
|
- "arcadia.name=Motor Contábil"
|
||||||
|
- "arcadia.type=python"
|
||||||
|
- "arcadia.category=contabil"
|
||||||
|
- "arcadia.port=8003"
|
||||||
|
- "arcadia.capabilities=lancamentos,dre,balancete,razao"
|
||||||
|
- "arcadia.requires_ia=false"
|
||||||
|
- "arcadia.health_path=/health"
|
||||||
|
- "arcadia.description=Contabilidade - Lançamentos, DRE, Balancete, Razão"
|
||||||
environment:
|
environment:
|
||||||
SERVICE_NAME: contabil
|
SERVICE_NAME: contabil
|
||||||
SERVICE_PORT: 8003
|
SERVICE_PORT: 8003
|
||||||
|
|
@ -100,6 +110,17 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "arcadia.discovery.enabled=true"
|
||||||
|
- "arcadia.name=Motor BI"
|
||||||
|
- "arcadia.type=python"
|
||||||
|
- "arcadia.category=bi"
|
||||||
|
- "arcadia.port=8004"
|
||||||
|
- "arcadia.capabilities=sql,charts,microbi,cache"
|
||||||
|
- "arcadia.requires_ia=true"
|
||||||
|
- "arcadia.ia_policies=bi,lgpd"
|
||||||
|
- "arcadia.health_path=/health"
|
||||||
|
- "arcadia.description=Business Intelligence - SQL, Charts, Micro-BI, Cache"
|
||||||
environment:
|
environment:
|
||||||
SERVICE_NAME: bi
|
SERVICE_NAME: bi
|
||||||
SERVICE_PORT: 8004
|
SERVICE_PORT: 8004
|
||||||
|
|
@ -117,6 +138,17 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "arcadia.discovery.enabled=true"
|
||||||
|
- "arcadia.name=Motor Automação"
|
||||||
|
- "arcadia.type=python"
|
||||||
|
- "arcadia.category=automation"
|
||||||
|
- "arcadia.port=8005"
|
||||||
|
- "arcadia.capabilities=scheduler,eventbus,workflow"
|
||||||
|
- "arcadia.requires_ia=true"
|
||||||
|
- "arcadia.ia_policies=automation"
|
||||||
|
- "arcadia.health_path=/health"
|
||||||
|
- "arcadia.description=Scheduler, Event Bus, Workflow Executor"
|
||||||
environment:
|
environment:
|
||||||
SERVICE_NAME: automation
|
SERVICE_NAME: automation
|
||||||
SERVICE_PORT: 8005
|
SERVICE_PORT: 8005
|
||||||
|
|
@ -134,6 +166,17 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "arcadia.discovery.enabled=true"
|
||||||
|
- "arcadia.name=Motor Fiscal"
|
||||||
|
- "arcadia.type=python"
|
||||||
|
- "arcadia.category=fiscal"
|
||||||
|
- "arcadia.port=8002"
|
||||||
|
- "arcadia.capabilities=nfe,nfce,ncm,cfop,cest,sefaz"
|
||||||
|
- "arcadia.requires_ia=true"
|
||||||
|
- "arcadia.ia_policies=fiscal"
|
||||||
|
- "arcadia.health_path=/health"
|
||||||
|
- "arcadia.description=NF-e/NFC-e - NCMs, CFOPs, CESTs, SEFAZ"
|
||||||
environment:
|
environment:
|
||||||
SERVICE_NAME: fisco
|
SERVICE_NAME: fisco
|
||||||
SERVICE_PORT: 8002
|
SERVICE_PORT: 8002
|
||||||
|
|
@ -151,6 +194,17 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.python
|
dockerfile: Dockerfile.python
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "arcadia.discovery.enabled=true"
|
||||||
|
- "arcadia.name=Serviço Embeddings"
|
||||||
|
- "arcadia.type=python"
|
||||||
|
- "arcadia.category=ai"
|
||||||
|
- "arcadia.port=8001"
|
||||||
|
- "arcadia.capabilities=embeddings,vectorsearch"
|
||||||
|
- "arcadia.requires_ia=true"
|
||||||
|
- "arcadia.ia_policies=ai,lgpd"
|
||||||
|
- "arcadia.health_path=/health"
|
||||||
|
- "arcadia.description=Embeddings e Vector Search com pgvector"
|
||||||
environment:
|
environment:
|
||||||
SERVICE_NAME: embeddings
|
SERVICE_NAME: embeddings
|
||||||
SERVICE_PORT: 8001
|
SERVICE_PORT: 8001
|
||||||
|
|
@ -165,6 +219,17 @@ services:
|
||||||
superset:
|
superset:
|
||||||
image: apache/superset:4.1.0
|
image: apache/superset:4.1.0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
labels:
|
||||||
|
- "arcadia.discovery.enabled=true"
|
||||||
|
- "arcadia.name=MetaSet BI"
|
||||||
|
- "arcadia.type=java"
|
||||||
|
- "arcadia.category=data"
|
||||||
|
- "arcadia.port=8088"
|
||||||
|
- "arcadia.capabilities=dashboards,charts,sql_lab,datasets"
|
||||||
|
- "arcadia.requires_ia=false"
|
||||||
|
- "arcadia.health_path=/health"
|
||||||
|
- "arcadia.description=Motor de BI - Consultas, Dashboards, Gráficos, Análises"
|
||||||
|
- "arcadia.version=4.1.0"
|
||||||
environment:
|
environment:
|
||||||
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod}
|
||||||
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
|
DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* API Routes - Rotas REST do Kernel
|
* API Routes - Rotas REST do Kernel
|
||||||
* Endpoints para controle e monitoramento dos serviços
|
* Endpoints para controle e monitoramento dos serviços
|
||||||
|
*
|
||||||
|
* V2.0 - Adicionado rotas do Service Registry (/api/registry/*)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { ProcessManager } from '../core/ProcessManager';
|
import { ProcessManager } from '../core/ProcessManager';
|
||||||
import { HealthMonitor } from '../core/HealthMonitor';
|
import { HealthMonitor } from '../core/HealthMonitor';
|
||||||
import { LogAggregator } from '../core/LogAggregator';
|
import { LogAggregator } from '../core/LogAggregator';
|
||||||
import { ApiResponse, ServiceActionRequest, ServiceLogsRequest } from '../types';
|
import { ServiceRegistry } from '../registry/ServiceRegistry';
|
||||||
|
import { ApiResponse, ServiceActionRequest } from '../types';
|
||||||
|
|
||||||
export function createKernelRoutes(
|
export function createKernelRoutes(
|
||||||
processManager: ProcessManager,
|
processManager: ProcessManager,
|
||||||
healthMonitor: HealthMonitor,
|
healthMonitor: HealthMonitor,
|
||||||
logAggregator: LogAggregator
|
logAggregator: LogAggregator,
|
||||||
|
serviceRegistry?: ServiceRegistry // NOVO: opcional
|
||||||
): Router {
|
): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|
@ -20,17 +24,12 @@ export function createKernelRoutes(
|
||||||
router.use(expressJson());
|
router.use(expressJson());
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// SERVICES ENDPOINTS
|
// SERVICES ENDPOINTS (ProcessManager)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/services
|
|
||||||
* Lista todos os serviços com seu estado atual
|
|
||||||
*/
|
|
||||||
router.get('/services', (req: Request, res: Response) => {
|
router.get('/services', (req: Request, res: Response) => {
|
||||||
const states = processManager.getAllStates();
|
const states = processManager.getAllStates();
|
||||||
|
|
||||||
// Adiciona health status de cada serviço
|
|
||||||
const servicesWithHealth = states.map(state => ({
|
const servicesWithHealth = states.map(state => ({
|
||||||
...state,
|
...state,
|
||||||
health: healthMonitor.getHealth(state.config.id),
|
health: healthMonitor.getHealth(state.config.id),
|
||||||
|
|
@ -39,10 +38,6 @@ export function createKernelRoutes(
|
||||||
res.json(successResponse(servicesWithHealth));
|
res.json(successResponse(servicesWithHealth));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/services/:id
|
|
||||||
* Detalhes de um serviço específico
|
|
||||||
*/
|
|
||||||
router.get('/services/:id', (req: Request, res: Response) => {
|
router.get('/services/:id', (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const state = processManager.getServiceState(id);
|
const state = processManager.getServiceState(id);
|
||||||
|
|
@ -57,10 +52,6 @@ export function createKernelRoutes(
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/kernel/services/:id/start
|
|
||||||
* Inicia um serviço
|
|
||||||
*/
|
|
||||||
router.post('/services/:id/start', async (req: Request, res: Response) => {
|
router.post('/services/:id/start', async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
|
|
@ -75,10 +66,6 @@ export function createKernelRoutes(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/kernel/services/:id/stop
|
|
||||||
* Para um serviço
|
|
||||||
*/
|
|
||||||
router.post('/services/:id/stop', async (req: Request, res: Response) => {
|
router.post('/services/:id/stop', async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { force } = req.body as ServiceActionRequest;
|
const { force } = req.body as ServiceActionRequest;
|
||||||
|
|
@ -94,10 +81,6 @@ export function createKernelRoutes(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/kernel/services/:id/restart
|
|
||||||
* Reinicia um serviço
|
|
||||||
*/
|
|
||||||
router.post('/services/:id/restart', async (req: Request, res: Response) => {
|
router.post('/services/:id/restart', async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
|
|
@ -112,10 +95,6 @@ export function createKernelRoutes(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/kernel/services/:id/kill
|
|
||||||
* Mata um serviço (force)
|
|
||||||
*/
|
|
||||||
router.post('/services/:id/kill', async (req: Request, res: Response) => {
|
router.post('/services/:id/kill', async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
|
|
@ -134,10 +113,6 @@ export function createKernelRoutes(
|
||||||
// LOGS ENDPOINTS
|
// LOGS ENDPOINTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/services/:id/logs
|
|
||||||
* Logs de um serviço específico
|
|
||||||
*/
|
|
||||||
router.get('/services/:id/logs', (req: Request, res: Response) => {
|
router.get('/services/:id/logs', (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { lines = '100', level } = req.query;
|
const { lines = '100', level } = req.query;
|
||||||
|
|
@ -147,7 +122,6 @@ export function createKernelRoutes(
|
||||||
parseInt(lines as string, 10) || 100
|
parseInt(lines as string, 10) || 100
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtra por nível se especificado
|
|
||||||
let filteredLogs = logs;
|
let filteredLogs = logs;
|
||||||
if (level) {
|
if (level) {
|
||||||
const levels = ['debug', 'info', 'warn', 'error'];
|
const levels = ['debug', 'info', 'warn', 'error'];
|
||||||
|
|
@ -158,10 +132,6 @@ export function createKernelRoutes(
|
||||||
res.json(successResponse(filteredLogs));
|
res.json(successResponse(filteredLogs));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/logs
|
|
||||||
* Logs de todos os serviços
|
|
||||||
*/
|
|
||||||
router.get('/logs', (req: Request, res: Response) => {
|
router.get('/logs', (req: Request, res: Response) => {
|
||||||
const { lines = '100', service, level } = req.query;
|
const { lines = '100', service, level } = req.query;
|
||||||
|
|
||||||
|
|
@ -174,10 +144,6 @@ export function createKernelRoutes(
|
||||||
res.json(successResponse(logs));
|
res.json(successResponse(logs));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/kernel/logs
|
|
||||||
* Limpa logs
|
|
||||||
*/
|
|
||||||
router.delete('/logs', (req: Request, res: Response) => {
|
router.delete('/logs', (req: Request, res: Response) => {
|
||||||
const { service } = req.query;
|
const { service } = req.query;
|
||||||
|
|
||||||
|
|
@ -194,10 +160,6 @@ export function createKernelRoutes(
|
||||||
// HEALTH ENDPOINTS
|
// HEALTH ENDPOINTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/health
|
|
||||||
* Health check geral do kernel
|
|
||||||
*/
|
|
||||||
router.get('/health', (req: Request, res: Response) => {
|
router.get('/health', (req: Request, res: Response) => {
|
||||||
const states = processManager.getAllStates();
|
const states = processManager.getAllStates();
|
||||||
const allHealth = healthMonitor.getAllHealth();
|
const allHealth = healthMonitor.getAllHealth();
|
||||||
|
|
@ -231,14 +193,9 @@ export function createKernelRoutes(
|
||||||
res.json(successResponse(health));
|
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) => {
|
router.get('/services/:id/health', async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// Força um check imediato
|
|
||||||
const health = await healthMonitor.forceCheck(id);
|
const health = await healthMonitor.forceCheck(id);
|
||||||
|
|
||||||
res.json(successResponse({
|
res.json(successResponse({
|
||||||
|
|
@ -252,10 +209,6 @@ export function createKernelRoutes(
|
||||||
// ACTIONS ENDPOINTS
|
// ACTIONS ENDPOINTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/kernel/actions/start-all
|
|
||||||
* Inicia todos os serviços
|
|
||||||
*/
|
|
||||||
router.post('/actions/start-all', async (req: Request, res: Response) => {
|
router.post('/actions/start-all', async (req: Request, res: Response) => {
|
||||||
const { auto = true } = req.body;
|
const { auto = true } = req.body;
|
||||||
|
|
||||||
|
|
@ -264,14 +217,9 @@ export function createKernelRoutes(
|
||||||
auto,
|
auto,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Executa em background
|
|
||||||
processManager.startAll(auto).catch(() => {});
|
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) => {
|
router.post('/actions/stop-all', async (req: Request, res: Response) => {
|
||||||
const { force = false } = req.body;
|
const { force = false } = req.body;
|
||||||
|
|
||||||
|
|
@ -284,10 +232,6 @@ export function createKernelRoutes(
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/kernel/actions/restart-all
|
|
||||||
* Reinicia todos os serviços
|
|
||||||
*/
|
|
||||||
router.post('/actions/restart-all', async (req: Request, res: Response) => {
|
router.post('/actions/restart-all', async (req: Request, res: Response) => {
|
||||||
const states = processManager.getAllStates();
|
const states = processManager.getAllStates();
|
||||||
|
|
||||||
|
|
@ -296,7 +240,6 @@ export function createKernelRoutes(
|
||||||
count: states.filter(s => s.status === 'running').length,
|
count: states.filter(s => s.status === 'running').length,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Executa em background
|
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
if (state.status === 'running') {
|
if (state.status === 'running') {
|
||||||
try {
|
try {
|
||||||
|
|
@ -312,10 +255,6 @@ export function createKernelRoutes(
|
||||||
// STATS ENDPOINTS
|
// STATS ENDPOINTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/stats
|
|
||||||
* Estatísticas do kernel
|
|
||||||
*/
|
|
||||||
router.get('/stats', (req: Request, res: Response) => {
|
router.get('/stats', (req: Request, res: Response) => {
|
||||||
const states = processManager.getAllStates();
|
const states = processManager.getAllStates();
|
||||||
const logStats = logAggregator.getStats();
|
const logStats = logAggregator.getStats();
|
||||||
|
|
@ -344,13 +283,9 @@ export function createKernelRoutes(
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// MISC ENDPOINTS
|
// CONFIG ENDPOINTS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/kernel/config
|
|
||||||
* Configuração do kernel
|
|
||||||
*/
|
|
||||||
router.get('/config', (req: Request, res: Response) => {
|
router.get('/config', (req: Request, res: Response) => {
|
||||||
const states = processManager.getAllStates();
|
const states = processManager.getAllStates();
|
||||||
|
|
||||||
|
|
@ -366,10 +301,88 @@ export function createKernelRoutes(
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// SERVICE REGISTRY ENDPOINTS (NOVO)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
if (serviceRegistry) {
|
||||||
|
// Lista todos os serviços descobertos
|
||||||
|
router.get('/registry/services', (req: Request, res: Response) => {
|
||||||
|
const { category, type, healthy, capability } = req.query;
|
||||||
|
|
||||||
|
const services = serviceRegistry.getServices({
|
||||||
|
category: category as any,
|
||||||
|
type: type as string,
|
||||||
|
healthyOnly: healthy === 'true',
|
||||||
|
capability: capability as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
services,
|
||||||
|
total: services.length,
|
||||||
|
timestamp: new Date(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Estatísticas do registry
|
||||||
|
router.get('/registry/stats', (req: Request, res: Response) => {
|
||||||
|
const stats = serviceRegistry.getStats();
|
||||||
|
res.json(successResponse(stats));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Busca serviço específico no registry
|
||||||
|
router.get('/registry/services/:id', (req: Request, res: Response) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const service = serviceRegistry.getService(id);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
return res.status(404).json(errorResponse(`Serviço ${id} não encontrado no registry`));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(successResponse(service));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Busca por categoria
|
||||||
|
router.get('/registry/by-category/:category', (req: Request, res: Response) => {
|
||||||
|
const { category } = req.params;
|
||||||
|
const services = serviceRegistry.getByCategory(category as any);
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
category,
|
||||||
|
services,
|
||||||
|
count: services.length,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Busca por capability
|
||||||
|
router.get('/registry/by-capability/:capability', (req: Request, res: Response) => {
|
||||||
|
const { capability } = req.params;
|
||||||
|
const services = serviceRegistry.getByCapability(capability);
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
capability,
|
||||||
|
services,
|
||||||
|
count: services.length,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger manual de discovery
|
||||||
|
router.post('/registry/discover', async (req: Request, res: Response) => {
|
||||||
|
// O discovery roda automaticamente, mas podemos forçar um health check
|
||||||
|
const services = serviceRegistry.getServices();
|
||||||
|
|
||||||
|
res.json(successResponse({
|
||||||
|
message: 'Discovery executado',
|
||||||
|
servicesFound: services.length,
|
||||||
|
timestamp: new Date(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper para JSON middleware (evita import do express inteiro)
|
// Helper para JSON middleware
|
||||||
function expressJson() {
|
function expressJson() {
|
||||||
return (req: Request, res: Response, next: any) => {
|
return (req: Request, res: Response, next: any) => {
|
||||||
if (req.headers['content-type']?.includes('application/json')) {
|
if (req.headers['content-type']?.includes('application/json')) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
/**
|
||||||
|
* Docker Discovery Provider
|
||||||
|
* Descobre serviços via labels em containers Docker
|
||||||
|
* Arcadia Kernel - Módulo de Service Discovery
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { DiscoveryProvider, RegisteredService, DockerLabelConfig, ServiceCapability, ServiceCategory } from '../registry/types';
|
||||||
|
import { HealthStatus } from '../types';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
// Prefixo para labels do Arcadia
|
||||||
|
const LABEL_PREFIX = 'arcadia.';
|
||||||
|
|
||||||
|
// Labels suportados
|
||||||
|
const LABELS = {
|
||||||
|
ENABLED: `${LABEL_PREFIX}discovery.enabled`,
|
||||||
|
NAME: `${LABEL_PREFIX}name`,
|
||||||
|
TYPE: `${LABEL_PREFIX}type`,
|
||||||
|
CATEGORY: `${LABEL_PREFIX}category`,
|
||||||
|
PORT: `${LABEL_PREFIX}port`,
|
||||||
|
CAPABILITIES: `${LABEL_PREFIX}capabilities`,
|
||||||
|
REQUIRES_IA: `${LABEL_PREFIX}requires_ia`,
|
||||||
|
IA_POLICIES: `${LABEL_PREFIX}ia_policies`,
|
||||||
|
HEALTH_PATH: `${LABEL_PREFIX}health_path`,
|
||||||
|
DESCRIPTION: `${LABEL_PREFIX}description`,
|
||||||
|
VERSION: `${LABEL_PREFIX}version`,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface DockerDiscoveryOptions {
|
||||||
|
labelFilter?: string;
|
||||||
|
networkName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DockerDiscovery implements DiscoveryProvider {
|
||||||
|
public readonly name = 'DockerDiscovery';
|
||||||
|
private options: DockerDiscoveryOptions;
|
||||||
|
private isDockerAvailable = false;
|
||||||
|
|
||||||
|
constructor(options: DockerDiscoveryOptions = {}) {
|
||||||
|
this.options = {
|
||||||
|
networkName: 'arcadia',
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAvailable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync('docker ps --format "{{.ID}}" | head -1');
|
||||||
|
this.isDockerAvailable = true;
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
this.isDockerAvailable = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async discover(): Promise<RegisteredService[]> {
|
||||||
|
if (!this.isDockerAvailable) {
|
||||||
|
throw new Error('Docker não está disponível');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(
|
||||||
|
`docker ps --filter "label=${LABELS.ENABLED}=true" --format "{{.ID}}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
const containerIds = stdout.trim().split('\n').filter(id => id);
|
||||||
|
|
||||||
|
if (containerIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const services: RegisteredService[] = [];
|
||||||
|
|
||||||
|
for (const containerId of containerIds) {
|
||||||
|
try {
|
||||||
|
const service = await this.parseContainer(containerId);
|
||||||
|
if (service) {
|
||||||
|
services.push(service);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[DockerDiscovery] Erro ao parsear container ${containerId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DockerDiscovery] Erro ao descobrir serviços:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseContainer(containerId: string): Promise<RegisteredService | null> {
|
||||||
|
const { stdout: labelsJson } = await execAsync(
|
||||||
|
`docker inspect --format='{{json .Config.Labels}}' ${containerId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const labels: Record<string, string> = JSON.parse(labelsJson);
|
||||||
|
|
||||||
|
if (labels[LABELS.ENABLED] !== 'true') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.extractConfig(labels);
|
||||||
|
if (!config.name || !config.port) {
|
||||||
|
console.warn(`[DockerDiscovery] Container ${containerId} não tem name ou port configurados`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout: containerJson } = await execAsync(
|
||||||
|
`docker inspect --format='{{json .}}' ${containerId}`
|
||||||
|
);
|
||||||
|
const containerInfo = JSON.parse(containerJson);
|
||||||
|
|
||||||
|
const hostname = this.getContainerHostname(containerInfo);
|
||||||
|
const networkAliases = this.getNetworkAliases(containerInfo);
|
||||||
|
const capabilities = this.parseCapabilities(config.capabilities);
|
||||||
|
const serviceId = this.generateServiceId(config.name, containerId);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: serviceId,
|
||||||
|
name: this.sanitizeName(config.name),
|
||||||
|
displayName: config.name,
|
||||||
|
type: config.type as any,
|
||||||
|
category: config.category,
|
||||||
|
description: config.description,
|
||||||
|
host: hostname,
|
||||||
|
port: config.port,
|
||||||
|
endpoint: `http://${hostname}:${config.port}`,
|
||||||
|
healthCheckPath: config.healthCheckPath || '/health',
|
||||||
|
healthStatus: 'unknown' as HealthStatus,
|
||||||
|
capabilities,
|
||||||
|
requiresIA: config.requiresIA || false,
|
||||||
|
iaPolicies: config.iaPolicies ? config.iaPolicies.split(',') : undefined,
|
||||||
|
metadata: {
|
||||||
|
source: 'docker',
|
||||||
|
discoveredAt: now,
|
||||||
|
lastUpdated: now,
|
||||||
|
labels,
|
||||||
|
dockerContainerId: containerId,
|
||||||
|
dockerImage: containerInfo.Config?.Image,
|
||||||
|
networkAliases,
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
version: config.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractConfig(labels: Record<string, string>): DockerLabelConfig {
|
||||||
|
return {
|
||||||
|
enabled: labels[LABELS.ENABLED] === 'true',
|
||||||
|
name: labels[LABELS.NAME] || '',
|
||||||
|
type: labels[LABELS.TYPE] || 'docker',
|
||||||
|
category: (labels[LABELS.CATEGORY] as ServiceCategory) || 'core',
|
||||||
|
port: parseInt(labels[LABELS.PORT] || '0', 10),
|
||||||
|
capabilities: labels[LABELS.CAPABILITIES],
|
||||||
|
requiresIA: labels[LABELS.REQUIRES_IA] === 'true',
|
||||||
|
iaPolicies: labels[LABELS.IA_POLICIES],
|
||||||
|
healthCheckPath: labels[LABELS.HEALTH_PATH],
|
||||||
|
description: labels[LABELS.DESCRIPTION],
|
||||||
|
version: labels[LABELS.VERSION],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCapabilities(capabilitiesJson?: string): ServiceCapability[] {
|
||||||
|
if (!capabilitiesJson) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(capabilitiesJson);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map(cap => ({
|
||||||
|
name: cap.name || cap,
|
||||||
|
version: cap.version || '1.0.0',
|
||||||
|
endpoints: cap.endpoints || [`/${cap.name || cap}`],
|
||||||
|
description: cap.description,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return capabilitiesJson.split(',').map(cap => ({
|
||||||
|
name: cap.trim(),
|
||||||
|
version: '1.0.0',
|
||||||
|
endpoints: [`/${cap.trim()}`],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContainerHostname(containerInfo: any): string {
|
||||||
|
const containerName = containerInfo.Name?.replace(/^\//, '');
|
||||||
|
if (containerName) {
|
||||||
|
return containerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const networkSettings = containerInfo.NetworkSettings;
|
||||||
|
if (networkSettings?.Networks) {
|
||||||
|
const network = networkSettings.Networks[this.options.networkName!];
|
||||||
|
if (network?.Aliases?.length > 0) {
|
||||||
|
return network.Aliases[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerInfo.Id?.substring(0, 12) || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNetworkAliases(containerInfo: any): string[] {
|
||||||
|
const aliases: string[] = [];
|
||||||
|
const networkSettings = containerInfo.NetworkSettings;
|
||||||
|
|
||||||
|
if (networkSettings?.Networks) {
|
||||||
|
for (const networkName of Object.keys(networkSettings.Networks)) {
|
||||||
|
const network = networkSettings.Networks[networkName];
|
||||||
|
if (network.Aliases) {
|
||||||
|
aliases.push(...network.Aliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(aliases)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateServiceId(name: string, containerId: string): string {
|
||||||
|
const sanitized = this.sanitizeName(name);
|
||||||
|
const shortId = containerId.substring(0, 8);
|
||||||
|
return `docker-${sanitized}-${shortId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitizeName(name: string): string {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/g, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Discovery Module - Exporta providers de descoberta
|
||||||
|
* Arcadia Kernel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { DockerDiscovery } from './DockerDiscovery';
|
||||||
|
export type { DockerDiscoveryOptions } from './DockerDiscovery';
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* Arcadia Kernel - Entry Point
|
* Arcadia Kernel - Entry Point
|
||||||
* Sistema nativo de gerenciamento de serviços
|
* Sistema nativo de gerenciamento de serviços
|
||||||
|
*
|
||||||
|
* V3.0 - Adicionado Service Registry + Docker Discovery
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
@ -12,12 +14,16 @@ import { LogAggregator } from './core/LogAggregator';
|
||||||
import { DashboardServer } from './dashboard/DashboardServer';
|
import { DashboardServer } from './dashboard/DashboardServer';
|
||||||
import { ServiceConfig, ServiceState } from './types';
|
import { ServiceConfig, ServiceState } from './types';
|
||||||
|
|
||||||
|
// NOVO: Service Registry e Discovery
|
||||||
|
import { ServiceRegistry } from './registry/ServiceRegistry';
|
||||||
|
import { DockerDiscovery } from './discovery/DockerDiscovery';
|
||||||
|
import { RegistryConfig, RegisteredService } from './registry/types';
|
||||||
|
|
||||||
// Suporte a ESM e CommonJS (build do Docker)
|
// Suporte a ESM e CommonJS (build do Docker)
|
||||||
const __dirname = (() => {
|
const __dirname = (() => {
|
||||||
try {
|
try {
|
||||||
return dirname(fileURLToPath(import.meta.url));
|
return dirname(fileURLToPath(import.meta.url));
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback para CommonJS build
|
|
||||||
return join(process.cwd(), 'server', 'kernel');
|
return join(process.cwd(), 'server', 'kernel');
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
@ -30,6 +36,11 @@ export interface KernelOptions {
|
||||||
host?: string;
|
host?: string;
|
||||||
};
|
};
|
||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
|
// NOVO: Opções do Service Registry
|
||||||
|
registry?: {
|
||||||
|
enabled?: boolean;
|
||||||
|
config?: Partial<RegistryConfig>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ArcadiaKernel {
|
export class ArcadiaKernel {
|
||||||
|
|
@ -37,6 +48,8 @@ export class ArcadiaKernel {
|
||||||
public healthMonitor: HealthMonitor;
|
public healthMonitor: HealthMonitor;
|
||||||
public logAggregator: LogAggregator;
|
public logAggregator: LogAggregator;
|
||||||
public dashboard?: DashboardServer;
|
public dashboard?: DashboardServer;
|
||||||
|
// NOVO: Service Registry
|
||||||
|
public serviceRegistry?: ServiceRegistry;
|
||||||
|
|
||||||
private options: Required<KernelOptions>;
|
private options: Required<KernelOptions>;
|
||||||
private services: ServiceConfig[] = [];
|
private services: ServiceConfig[] = [];
|
||||||
|
|
@ -51,10 +64,19 @@ export class ArcadiaKernel {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
},
|
},
|
||||||
autoStart: false,
|
autoStart: false,
|
||||||
|
// NOVO: Registry ativado por padrão
|
||||||
|
registry: {
|
||||||
|
enabled: true,
|
||||||
|
config: {
|
||||||
|
enableAutoDiscovery: true,
|
||||||
|
discoveryInterval: 30000,
|
||||||
|
healthCheckInterval: 15000,
|
||||||
|
},
|
||||||
|
},
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inicializa componentes
|
// Inicializa componentes existentes
|
||||||
this.logAggregator = new LogAggregator({ maxLines: 10000 });
|
this.logAggregator = new LogAggregator({ maxLines: 10000 });
|
||||||
|
|
||||||
this.processManager = new ProcessManager({
|
this.processManager = new ProcessManager({
|
||||||
|
|
@ -76,6 +98,21 @@ export class ArcadiaKernel {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOVO: Inicializa Service Registry se habilitado
|
||||||
|
if (this.options.registry.enabled) {
|
||||||
|
this.serviceRegistry = new ServiceRegistry({
|
||||||
|
config: this.options.registry.config,
|
||||||
|
discoveryProviders: [
|
||||||
|
new DockerDiscovery({ networkName: 'arcadia' }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escuta eventos do registry
|
||||||
|
this.serviceRegistry.on('event', (event) => {
|
||||||
|
console.log(`[Registry] ${event.type}: ${event.serviceId}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Carrega configurações
|
// Carrega configurações
|
||||||
this.loadConfig();
|
this.loadConfig();
|
||||||
}
|
}
|
||||||
|
|
@ -89,13 +126,18 @@ export class ArcadiaKernel {
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Kernel] Iniciando Arcadia Kernel...');
|
console.log('[Kernel] Iniciando Arcadia Kernel...');
|
||||||
console.log(`[Kernel] ${this.services.length} serviços registrados`);
|
console.log(`[Kernel] ${this.services.length} serviços configurados (services.json)`);
|
||||||
|
|
||||||
// Registra serviços no ProcessManager
|
// Registra serviços no ProcessManager
|
||||||
for (const config of this.services) {
|
for (const config of this.services) {
|
||||||
this.processManager.registerService(config);
|
this.processManager.registerService(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOVO: Inicia Service Registry
|
||||||
|
if (this.serviceRegistry) {
|
||||||
|
await this.serviceRegistry.start();
|
||||||
|
}
|
||||||
|
|
||||||
// Inicia Dashboard
|
// Inicia Dashboard
|
||||||
if (this.options.dashboard.enabled) {
|
if (this.options.dashboard.enabled) {
|
||||||
this.dashboard = new DashboardServer(
|
this.dashboard = new DashboardServer(
|
||||||
|
|
@ -140,6 +182,9 @@ export class ArcadiaKernel {
|
||||||
|
|
||||||
console.log('[Kernel] Parando Kernel...');
|
console.log('[Kernel] Parando Kernel...');
|
||||||
|
|
||||||
|
// NOVO: Para Service Registry
|
||||||
|
this.serviceRegistry?.stop();
|
||||||
|
|
||||||
// Para health monitor
|
// Para health monitor
|
||||||
this.healthMonitor.stopAll();
|
this.healthMonitor.stopAll();
|
||||||
|
|
||||||
|
|
@ -160,6 +205,26 @@ export class ArcadiaKernel {
|
||||||
return this.started;
|
return this.started;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOVO: Retorna estatísticas combinadas (ProcessManager + Registry)
|
||||||
|
*/
|
||||||
|
getStats(): { processManager: any; registry: any } {
|
||||||
|
return {
|
||||||
|
processManager: {
|
||||||
|
total: this.services.length,
|
||||||
|
states: this.processManager.getAllStates(),
|
||||||
|
},
|
||||||
|
registry: this.serviceRegistry?.getStats(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOVO: Lista serviços descobertos (via Registry)
|
||||||
|
*/
|
||||||
|
getDiscoveredServices(): RegisteredService[] {
|
||||||
|
return this.serviceRegistry?.getServices() || [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carrega configurações dos serviços
|
* Carrega configurações dos serviços
|
||||||
*/
|
*/
|
||||||
|
|
@ -204,10 +269,21 @@ export { LogAggregator } from './core/LogAggregator';
|
||||||
export { DashboardServer } from './dashboard/DashboardServer';
|
export { DashboardServer } from './dashboard/DashboardServer';
|
||||||
export { KernelWebSocket } from './websocket/KernelWebSocket';
|
export { KernelWebSocket } from './websocket/KernelWebSocket';
|
||||||
export { createKernelRoutes } from './api/routes';
|
export { createKernelRoutes } from './api/routes';
|
||||||
|
|
||||||
|
// NOVO: Exports do Registry e Discovery
|
||||||
|
export { ServiceRegistry } from './registry/ServiceRegistry';
|
||||||
|
export { DockerDiscovery } from './discovery/DockerDiscovery';
|
||||||
|
export type {
|
||||||
|
RegisteredService,
|
||||||
|
ServiceFilter,
|
||||||
|
RegistryEvent,
|
||||||
|
DiscoverySource,
|
||||||
|
ServiceCategory,
|
||||||
|
} from './registry/types';
|
||||||
|
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
// Entry point para execução direta
|
// Entry point para execução direta
|
||||||
// Protegido para funcionar no build CommonJS do Docker
|
|
||||||
try {
|
try {
|
||||||
if (import.meta.url && import.meta.url === `file://${process.argv[1]}`) {
|
if (import.meta.url && import.meta.url === `file://${process.argv[1]}`) {
|
||||||
const kernel = new ArcadiaKernel({
|
const kernel = new ArcadiaKernel({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
/**
|
||||||
|
* Service Registry - Catálogo central de serviços
|
||||||
|
* Arcadia Kernel - Módulo de Service Discovery
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import { RegisteredService, ServiceFilter, RegistryEvent, RegistryConfig, DiscoveryProvider } from './types';
|
||||||
|
import { HealthStatus } from '../types';
|
||||||
|
|
||||||
|
export interface ServiceRegistryOptions {
|
||||||
|
config?: Partial<RegistryConfig>;
|
||||||
|
discoveryProviders?: DiscoveryProvider[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: RegistryConfig = {
|
||||||
|
enableAutoDiscovery: true,
|
||||||
|
discoveryInterval: 30000, // 30s
|
||||||
|
healthCheckInterval: 15000, // 15s
|
||||||
|
cleanupInterval: 60000, // 1min
|
||||||
|
maxStaleAge: 300000, // 5min
|
||||||
|
persistToDatabase: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ServiceRegistry extends EventEmitter {
|
||||||
|
private services: Map<string, RegisteredService> = new Map();
|
||||||
|
private config: RegistryConfig;
|
||||||
|
private discoveryProviders: DiscoveryProvider[] = [];
|
||||||
|
private discoveryTimer?: NodeJS.Timeout;
|
||||||
|
private healthCheckTimer?: NodeJS.Timeout;
|
||||||
|
private cleanupTimer?: NodeJS.Timeout;
|
||||||
|
private isRunning = false;
|
||||||
|
|
||||||
|
constructor(options: ServiceRegistryOptions = {}) {
|
||||||
|
super();
|
||||||
|
this.config = { ...DEFAULT_CONFIG, ...options.config };
|
||||||
|
this.discoveryProviders = options.discoveryProviders || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inicia o registry e discovery
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
|
||||||
|
console.log('[Registry] Iniciando Service Registry...');
|
||||||
|
|
||||||
|
// Verifica quais providers estão disponíveis
|
||||||
|
const availableProviders: DiscoveryProvider[] = [];
|
||||||
|
for (const provider of this.discoveryProviders) {
|
||||||
|
try {
|
||||||
|
const available = await provider.isAvailable();
|
||||||
|
if (available) {
|
||||||
|
availableProviders.push(provider);
|
||||||
|
console.log(`[Registry] Provider disponível: ${provider.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[Registry] Provider indisponível: ${provider.name}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[Registry] Erro ao verificar provider ${provider.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.discoveryProviders = availableProviders;
|
||||||
|
|
||||||
|
// Executa discovery inicial
|
||||||
|
await this.runDiscovery();
|
||||||
|
|
||||||
|
// Inicia timers
|
||||||
|
if (this.config.enableAutoDiscovery) {
|
||||||
|
this.discoveryTimer = setInterval(() => this.runDiscovery(), this.config.discoveryInterval);
|
||||||
|
}
|
||||||
|
this.healthCheckTimer = setInterval(() => this.runHealthChecks(), this.config.healthCheckInterval);
|
||||||
|
this.cleanupTimer = setInterval(() => this.cleanupStaleServices(), this.config.cleanupInterval);
|
||||||
|
|
||||||
|
this.isRunning = true;
|
||||||
|
console.log(`[Registry] Registry iniciado com ${this.services.size} serviços`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Para o registry
|
||||||
|
*/
|
||||||
|
stop(): void {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
console.log('[Registry] Parando Service Registry...');
|
||||||
|
|
||||||
|
if (this.discoveryTimer) clearInterval(this.discoveryTimer);
|
||||||
|
if (this.healthCheckTimer) clearInterval(this.healthCheckTimer);
|
||||||
|
if (this.cleanupTimer) clearInterval(this.cleanupTimer);
|
||||||
|
|
||||||
|
// Para os watchers dos providers
|
||||||
|
for (const provider of this.discoveryProviders) {
|
||||||
|
provider.stop?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isRunning = false;
|
||||||
|
console.log('[Registry] Registry parado');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registra um serviço manualmente
|
||||||
|
*/
|
||||||
|
register(service: RegisteredService): void {
|
||||||
|
const existing = this.services.get(service.id);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
// Atualiza serviço existente
|
||||||
|
this.services.set(service.id, {
|
||||||
|
...service,
|
||||||
|
metadata: {
|
||||||
|
...service.metadata,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.emitEvent('service:updated', service);
|
||||||
|
} else {
|
||||||
|
// Novo serviço
|
||||||
|
this.services.set(service.id, service);
|
||||||
|
this.emitEvent('service:registered', service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove um serviço do registry
|
||||||
|
*/
|
||||||
|
unregister(serviceId: string): boolean {
|
||||||
|
const service = this.services.get(serviceId);
|
||||||
|
if (!service) return false;
|
||||||
|
|
||||||
|
this.services.delete(serviceId);
|
||||||
|
this.emitEvent('service:unregistered', undefined, serviceId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca um serviço pelo ID
|
||||||
|
*/
|
||||||
|
getService(serviceId: string): RegisteredService | undefined {
|
||||||
|
return this.services.get(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista todos os serviços (com filtros opcionais)
|
||||||
|
*/
|
||||||
|
getServices(filter?: ServiceFilter): RegisteredService[] {
|
||||||
|
let services = Array.from(this.services.values());
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
if (filter.category) {
|
||||||
|
services = services.filter(s => s.category === filter.category);
|
||||||
|
}
|
||||||
|
if (filter.type) {
|
||||||
|
services = services.filter(s => s.type === filter.type);
|
||||||
|
}
|
||||||
|
if (filter.requiresIA !== undefined) {
|
||||||
|
services = services.filter(s => s.requiresIA === filter.requiresIA);
|
||||||
|
}
|
||||||
|
if (filter.capability) {
|
||||||
|
services = services.filter(s =>
|
||||||
|
s.capabilities.some(c => c.name === filter.capability)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filter.source) {
|
||||||
|
services = services.filter(s => s.metadata.source === filter.source);
|
||||||
|
}
|
||||||
|
if (filter.healthyOnly) {
|
||||||
|
services = services.filter(s => s.healthStatus === 'healthy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca serviços por categoria
|
||||||
|
*/
|
||||||
|
getByCategory(category: string): RegisteredService[] {
|
||||||
|
return this.getServices({ category: category as any });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca serviço por capability
|
||||||
|
*/
|
||||||
|
getByCapability(capabilityName: string): RegisteredService[] {
|
||||||
|
return this.getServices({ capability: capabilityName });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atualiza status de saúde de um serviço
|
||||||
|
*/
|
||||||
|
updateHealth(serviceId: string, status: HealthStatus): void {
|
||||||
|
const service = this.services.get(serviceId);
|
||||||
|
if (!service) return;
|
||||||
|
|
||||||
|
const previousHealth = service.healthStatus;
|
||||||
|
if (previousHealth !== status) {
|
||||||
|
service.healthStatus = status;
|
||||||
|
service.lastHealthCheck = new Date();
|
||||||
|
this.emitEvent('service:health_changed', service, serviceId, previousHealth, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna estatísticas do registry
|
||||||
|
*/
|
||||||
|
getStats() {
|
||||||
|
const services = Array.from(this.services.values());
|
||||||
|
const byCategory: Record<string, number> = {};
|
||||||
|
|
||||||
|
for (const service of services) {
|
||||||
|
byCategory[service.category] = (byCategory[service.category] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: services.length,
|
||||||
|
healthy: services.filter(s => s.healthStatus === 'healthy').length,
|
||||||
|
unhealthy: services.filter(s => s.healthStatus === 'unhealthy').length,
|
||||||
|
unknown: services.filter(s => s.healthStatus === 'unknown').length,
|
||||||
|
byCategory,
|
||||||
|
bySource: this.groupBySource(services),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa discovery em todos os providers
|
||||||
|
*/
|
||||||
|
private async runDiscovery(): Promise<void> {
|
||||||
|
for (const provider of this.discoveryProviders) {
|
||||||
|
try {
|
||||||
|
const discoveredServices = await provider.discover();
|
||||||
|
|
||||||
|
for (const service of discoveredServices) {
|
||||||
|
const existing = this.services.get(service.id);
|
||||||
|
if (!existing) {
|
||||||
|
this.register(service);
|
||||||
|
} else {
|
||||||
|
// Atualiza metadata mas mantém health status
|
||||||
|
this.register({
|
||||||
|
...service,
|
||||||
|
healthStatus: existing.healthStatus,
|
||||||
|
lastHealthCheck: existing.lastHealthCheck,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Registry] Erro no discovery ${provider.name}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa health checks em todos os serviços
|
||||||
|
*/
|
||||||
|
private async runHealthChecks(): Promise<void> {
|
||||||
|
for (const service of this.services.values()) {
|
||||||
|
// Só verifica serviços que não são externos ou que estão habilitados
|
||||||
|
if (!service.enabled) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isHealthy = await this.checkServiceHealth(service);
|
||||||
|
const newStatus: HealthStatus = isHealthy ? 'healthy' : 'unhealthy';
|
||||||
|
this.updateHealth(service.id, newStatus);
|
||||||
|
} catch (error) {
|
||||||
|
this.updateHealth(service.id, 'unhealthy');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifica saúde de um serviço específico
|
||||||
|
*/
|
||||||
|
private async checkServiceHealth(service: RegisteredService): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
const response = await fetch(`${service.endpoint}${service.healthCheckPath}`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove serviços stale (sem update há muito tempo)
|
||||||
|
*/
|
||||||
|
private cleanupStaleServices(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const toRemove: string[] = [];
|
||||||
|
|
||||||
|
for (const [id, service] of this.services) {
|
||||||
|
// Só remove serviços descobertos automaticamente (não manuais)
|
||||||
|
if (service.metadata.source === 'manual') continue;
|
||||||
|
|
||||||
|
const age = now - service.metadata.lastUpdated.getTime();
|
||||||
|
if (age > this.config.maxStaleAge) {
|
||||||
|
toRemove.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id of toRemove) {
|
||||||
|
console.log(`[Registry] Removendo serviço stale: ${id}`);
|
||||||
|
this.unregister(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agrupa serviços por source
|
||||||
|
*/
|
||||||
|
private groupBySource(services: RegisteredService[]): Record<string, number> {
|
||||||
|
const result: Record<string, number> = {};
|
||||||
|
for (const service of services) {
|
||||||
|
const source = service.metadata.source;
|
||||||
|
result[source] = (result[source] || 0) + 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emite evento do registry
|
||||||
|
*/
|
||||||
|
private emitEvent(
|
||||||
|
type: RegistryEvent['type'],
|
||||||
|
service?: RegisteredService,
|
||||||
|
serviceId?: string,
|
||||||
|
previousHealth?: HealthStatus,
|
||||||
|
newHealth?: HealthStatus
|
||||||
|
): void {
|
||||||
|
const event: RegistryEvent = {
|
||||||
|
type,
|
||||||
|
timestamp: new Date(),
|
||||||
|
serviceId: service?.id || serviceId || 'unknown',
|
||||||
|
service,
|
||||||
|
previousHealth,
|
||||||
|
newHealth,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emit('event', event);
|
||||||
|
this.emit(type, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adiciona um provider de discovery dinamicamente
|
||||||
|
*/
|
||||||
|
addDiscoveryProvider(provider: DiscoveryProvider): void {
|
||||||
|
this.discoveryProviders.push(provider);
|
||||||
|
if (this.isRunning) {
|
||||||
|
provider.isAvailable().then(available => {
|
||||||
|
if (available) {
|
||||||
|
this.runDiscovery();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna se o registry está rodando
|
||||||
|
*/
|
||||||
|
running(): boolean {
|
||||||
|
return this.isRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Registry Module - Exporta Service Registry e tipos
|
||||||
|
* Arcadia Kernel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { ServiceRegistry } from './ServiceRegistry';
|
||||||
|
export type { ServiceRegistryOptions } from './ServiceRegistry';
|
||||||
|
export type {
|
||||||
|
DiscoverySource,
|
||||||
|
ServiceCategory,
|
||||||
|
ServiceCapability,
|
||||||
|
ServiceMetadata,
|
||||||
|
RegisteredService,
|
||||||
|
ServiceFilter,
|
||||||
|
RegistryEvent,
|
||||||
|
RegistryConfig,
|
||||||
|
DiscoveryProvider,
|
||||||
|
DockerLabelConfig,
|
||||||
|
RegistryApiResponse,
|
||||||
|
} from './types';
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/**
|
||||||
|
* Tipos para Service Discovery e Registry
|
||||||
|
* Arcadia Kernel - Service Registry Module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HealthStatus } from '../types';
|
||||||
|
|
||||||
|
// Tipos de fonte de descoberta
|
||||||
|
export type DiscoverySource = 'docker' | 'manual' | 'xos' | 'consul' | 'kubernetes';
|
||||||
|
|
||||||
|
// Categoria do serviço
|
||||||
|
export type ServiceCategory =
|
||||||
|
| 'erp'
|
||||||
|
| 'fiscal'
|
||||||
|
| 'contabil'
|
||||||
|
| 'bi'
|
||||||
|
| 'data'
|
||||||
|
| 'automation'
|
||||||
|
| 'communication'
|
||||||
|
| 'intelligence'
|
||||||
|
| 'ai'
|
||||||
|
| 'core';
|
||||||
|
|
||||||
|
// Capacidades que um serviço pode ter
|
||||||
|
export interface ServiceCapability {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
endpoints: string[];
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadados de um serviço
|
||||||
|
export interface ServiceMetadata {
|
||||||
|
source: DiscoverySource;
|
||||||
|
discoveredAt: Date;
|
||||||
|
lastUpdated: Date;
|
||||||
|
labels: Record<string, string>;
|
||||||
|
dockerContainerId?: string;
|
||||||
|
dockerImage?: string;
|
||||||
|
networkAliases?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Definição de um serviço no registry
|
||||||
|
export interface RegisteredService {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
type: 'python' | 'node' | 'java' | 'go' | 'rust' | 'docker';
|
||||||
|
category: ServiceCategory;
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
// Endereçamento
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
endpoint: string; // URL completa: http://host:port
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
healthCheckPath: string;
|
||||||
|
healthStatus: HealthStatus;
|
||||||
|
lastHealthCheck?: Date;
|
||||||
|
|
||||||
|
// Capacidades
|
||||||
|
capabilities: ServiceCapability[];
|
||||||
|
requiresIA: boolean;
|
||||||
|
iaPolicies?: string[]; // Ex: ['fiscal', 'lgpd']
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
metadata: ServiceMetadata;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
enabled: boolean;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtros para busca de serviços
|
||||||
|
export interface ServiceFilter {
|
||||||
|
category?: ServiceCategory;
|
||||||
|
type?: string;
|
||||||
|
requiresIA?: boolean;
|
||||||
|
capability?: string;
|
||||||
|
source?: DiscoverySource;
|
||||||
|
healthyOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evento de mudança no registry
|
||||||
|
export interface RegistryEvent {
|
||||||
|
type: 'service:registered' | 'service:unregistered' | 'service:updated' | 'service:health_changed';
|
||||||
|
timestamp: Date;
|
||||||
|
serviceId: string;
|
||||||
|
service?: RegisteredService;
|
||||||
|
previousHealth?: HealthStatus;
|
||||||
|
newHealth?: HealthStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configuração do registry
|
||||||
|
export interface RegistryConfig {
|
||||||
|
enableAutoDiscovery: boolean;
|
||||||
|
discoveryInterval: number; // ms
|
||||||
|
healthCheckInterval: number; // ms
|
||||||
|
cleanupInterval: number; // ms
|
||||||
|
maxStaleAge: number; // ms - tempo máximo sem health check
|
||||||
|
persistToDatabase: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface para descoberta (Docker, XOS, etc)
|
||||||
|
export interface DiscoveryProvider {
|
||||||
|
name: string;
|
||||||
|
isAvailable(): Promise<boolean>;
|
||||||
|
discover(): Promise<RegisteredService[]>;
|
||||||
|
watch?(callback: (services: RegisteredService[]) => void): void;
|
||||||
|
stop?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker-specific types
|
||||||
|
export interface DockerLabelConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
category: ServiceCategory;
|
||||||
|
port: number;
|
||||||
|
capabilities?: string; // JSON string
|
||||||
|
requiresIA?: boolean;
|
||||||
|
iaPolicies?: string; // Comma-separated
|
||||||
|
healthCheckPath?: string;
|
||||||
|
description?: string;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resposta da API de registry
|
||||||
|
export interface RegistryApiResponse {
|
||||||
|
services: RegisteredService[];
|
||||||
|
total: number;
|
||||||
|
healthy: number;
|
||||||
|
unhealthy: number;
|
||||||
|
byCategory: Record<ServiceCategory, number>;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue