From 7651167d7246c4e4c632b63079f8e90c216c3c4a Mon Sep 17 00:00:00 2001 From: Jonas Pacheco Date: Mon, 6 Apr 2026 11:02:49 -0300 Subject: [PATCH] feat(kernel): implementa Service Discovery e Registry (Semana 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docker-compose.yml | 65 ++++ server/kernel/api/routes.ts | 161 ++++----- server/kernel/discovery/DockerDiscovery.ts | 240 ++++++++++++++ server/kernel/discovery/index.ts | 7 + server/kernel/index.ts | 84 ++++- server/kernel/registry/ServiceRegistry.ts | 365 +++++++++++++++++++++ server/kernel/registry/index.ts | 20 ++ server/kernel/registry/types.ts | 136 ++++++++ 8 files changed, 1000 insertions(+), 78 deletions(-) create mode 100644 server/kernel/discovery/DockerDiscovery.ts create mode 100644 server/kernel/discovery/index.ts create mode 100644 server/kernel/registry/ServiceRegistry.ts create mode 100644 server/kernel/registry/index.ts create mode 100644 server/kernel/registry/types.ts diff --git a/docker-compose.yml b/docker-compose.yml index 259912f..46ddd84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -83,6 +83,16 @@ services: context: . dockerfile: Dockerfile.python 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: SERVICE_NAME: contabil SERVICE_PORT: 8003 @@ -100,6 +110,17 @@ services: context: . dockerfile: Dockerfile.python 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: SERVICE_NAME: bi SERVICE_PORT: 8004 @@ -117,6 +138,17 @@ services: context: . dockerfile: Dockerfile.python 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: SERVICE_NAME: automation SERVICE_PORT: 8005 @@ -134,6 +166,17 @@ services: context: . dockerfile: Dockerfile.python 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: SERVICE_NAME: fisco SERVICE_PORT: 8002 @@ -151,6 +194,17 @@ services: context: . dockerfile: Dockerfile.python 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: SERVICE_NAME: embeddings SERVICE_PORT: 8001 @@ -165,6 +219,17 @@ services: superset: image: apache/superset:4.1.0 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: SUPERSET_SECRET_KEY: ${SUPERSET_SECRET_KEY:-superset-secret-change-in-prod} DATABASE_URL: postgresql://${PGUSER:-arcadia}:${PGPASSWORD:-arcadia123}@db:5432/arcadia_superset diff --git a/server/kernel/api/routes.ts b/server/kernel/api/routes.ts index 414878b..a3c6fc9 100644 --- a/server/kernel/api/routes.ts +++ b/server/kernel/api/routes.ts @@ -1,18 +1,22 @@ /** * API Routes - Rotas REST do Kernel * Endpoints para controle e monitoramento dos serviços + * + * V2.0 - Adicionado rotas do Service Registry (/api/registry/*) */ 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'; +import { ServiceRegistry } from '../registry/ServiceRegistry'; +import { ApiResponse, ServiceActionRequest } from '../types'; export function createKernelRoutes( processManager: ProcessManager, healthMonitor: HealthMonitor, - logAggregator: LogAggregator + logAggregator: LogAggregator, + serviceRegistry?: ServiceRegistry // NOVO: opcional ): Router { const router = Router(); @@ -20,17 +24,12 @@ export function createKernelRoutes( 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) => { const states = processManager.getAllStates(); - // Adiciona health status de cada serviço const servicesWithHealth = states.map(state => ({ ...state, health: healthMonitor.getHealth(state.config.id), @@ -39,10 +38,6 @@ export function createKernelRoutes( 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); @@ -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) => { 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) => { const { id } = req.params; 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) => { 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) => { const { id } = req.params; @@ -134,10 +113,6 @@ export function createKernelRoutes( // 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; @@ -147,7 +122,6 @@ export function createKernelRoutes( parseInt(lines as string, 10) || 100 ); - // Filtra por nível se especificado let filteredLogs = logs; if (level) { const levels = ['debug', 'info', 'warn', 'error']; @@ -158,10 +132,6 @@ export function createKernelRoutes( 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; @@ -174,10 +144,6 @@ export function createKernelRoutes( res.json(successResponse(logs)); }); - /** - * DELETE /api/kernel/logs - * Limpa logs - */ router.delete('/logs', (req: Request, res: Response) => { const { service } = req.query; @@ -194,10 +160,6 @@ export function createKernelRoutes( // 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(); @@ -231,14 +193,9 @@ export function createKernelRoutes( 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({ @@ -252,10 +209,6 @@ export function createKernelRoutes( // 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; @@ -264,14 +217,9 @@ export function createKernelRoutes( 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; @@ -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) => { const states = processManager.getAllStates(); @@ -296,7 +240,6 @@ export function createKernelRoutes( count: states.filter(s => s.status === 'running').length, })); - // Executa em background for (const state of states) { if (state.status === 'running') { try { @@ -312,10 +255,6 @@ export function createKernelRoutes( // 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(); @@ -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) => { 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; } -// Helper para JSON middleware (evita import do express inteiro) +// Helper para JSON middleware function expressJson() { return (req: Request, res: Response, next: any) => { if (req.headers['content-type']?.includes('application/json')) { diff --git a/server/kernel/discovery/DockerDiscovery.ts b/server/kernel/discovery/DockerDiscovery.ts new file mode 100644 index 0000000..3a14d11 --- /dev/null +++ b/server/kernel/discovery/DockerDiscovery.ts @@ -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 { + try { + await execAsync('docker ps --format "{{.ID}}" | head -1'); + this.isDockerAvailable = true; + return true; + } catch { + this.isDockerAvailable = false; + return false; + } + } + + async discover(): Promise { + 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 { + const { stdout: labelsJson } = await execAsync( + `docker inspect --format='{{json .Config.Labels}}' ${containerId}` + ); + + const labels: Record = 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): 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, ''); + } +} diff --git a/server/kernel/discovery/index.ts b/server/kernel/discovery/index.ts new file mode 100644 index 0000000..5609825 --- /dev/null +++ b/server/kernel/discovery/index.ts @@ -0,0 +1,7 @@ +/** + * Discovery Module - Exporta providers de descoberta + * Arcadia Kernel + */ + +export { DockerDiscovery } from './DockerDiscovery'; +export type { DockerDiscoveryOptions } from './DockerDiscovery'; diff --git a/server/kernel/index.ts b/server/kernel/index.ts index 186fc7b..77850d0 100644 --- a/server/kernel/index.ts +++ b/server/kernel/index.ts @@ -1,6 +1,8 @@ /** * Arcadia Kernel - Entry Point * Sistema nativo de gerenciamento de serviços + * + * V3.0 - Adicionado Service Registry + Docker Discovery */ import { readFileSync } from 'fs'; @@ -12,12 +14,16 @@ import { LogAggregator } from './core/LogAggregator'; import { DashboardServer } from './dashboard/DashboardServer'; 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) const __dirname = (() => { try { return dirname(fileURLToPath(import.meta.url)); } catch { - // Fallback para CommonJS build return join(process.cwd(), 'server', 'kernel'); } })(); @@ -30,6 +36,11 @@ export interface KernelOptions { host?: string; }; autoStart?: boolean; + // NOVO: Opções do Service Registry + registry?: { + enabled?: boolean; + config?: Partial; + }; } export class ArcadiaKernel { @@ -37,6 +48,8 @@ export class ArcadiaKernel { public healthMonitor: HealthMonitor; public logAggregator: LogAggregator; public dashboard?: DashboardServer; + // NOVO: Service Registry + public serviceRegistry?: ServiceRegistry; private options: Required; private services: ServiceConfig[] = []; @@ -51,10 +64,19 @@ export class ArcadiaKernel { host: '0.0.0.0', }, autoStart: false, + // NOVO: Registry ativado por padrão + registry: { + enabled: true, + config: { + enableAutoDiscovery: true, + discoveryInterval: 30000, + healthCheckInterval: 15000, + }, + }, ...options, }; - // Inicializa componentes + // Inicializa componentes existentes this.logAggregator = new LogAggregator({ maxLines: 10000 }); 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 this.loadConfig(); } @@ -89,13 +126,18 @@ export class ArcadiaKernel { } 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 for (const config of this.services) { this.processManager.registerService(config); } + // NOVO: Inicia Service Registry + if (this.serviceRegistry) { + await this.serviceRegistry.start(); + } + // Inicia Dashboard if (this.options.dashboard.enabled) { this.dashboard = new DashboardServer( @@ -140,6 +182,9 @@ export class ArcadiaKernel { console.log('[Kernel] Parando Kernel...'); + // NOVO: Para Service Registry + this.serviceRegistry?.stop(); + // Para health monitor this.healthMonitor.stopAll(); @@ -160,6 +205,26 @@ export class ArcadiaKernel { 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 */ @@ -204,10 +269,21 @@ export { LogAggregator } from './core/LogAggregator'; export { DashboardServer } from './dashboard/DashboardServer'; export { KernelWebSocket } from './websocket/KernelWebSocket'; 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'; // Entry point para execução direta -// Protegido para funcionar no build CommonJS do Docker try { if (import.meta.url && import.meta.url === `file://${process.argv[1]}`) { const kernel = new ArcadiaKernel({ diff --git a/server/kernel/registry/ServiceRegistry.ts b/server/kernel/registry/ServiceRegistry.ts new file mode 100644 index 0000000..8ca2e7d --- /dev/null +++ b/server/kernel/registry/ServiceRegistry.ts @@ -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; + 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 = 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 { + 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 = {}; + + 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 { + 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 { + 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 { + 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 { + const result: Record = {}; + 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; + } +} diff --git a/server/kernel/registry/index.ts b/server/kernel/registry/index.ts new file mode 100644 index 0000000..7aa9338 --- /dev/null +++ b/server/kernel/registry/index.ts @@ -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'; diff --git a/server/kernel/registry/types.ts b/server/kernel/registry/types.ts new file mode 100644 index 0000000..201d2fb --- /dev/null +++ b/server/kernel/registry/types.ts @@ -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; + 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; + discover(): Promise; + 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; +}