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:
Jonas Pacheco 2026-04-06 11:02:49 -03:00
parent 2916fb04ae
commit 7651167d72
8 changed files with 1000 additions and 78 deletions

View File

@ -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

View File

@ -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')) {

View File

@ -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, '');
}
}

View File

@ -0,0 +1,7 @@
/**
* Discovery Module - Exporta providers de descoberta
* Arcadia Kernel
*/
export { DockerDiscovery } from './DockerDiscovery';
export type { DockerDiscoveryOptions } from './DockerDiscovery';

View File

@ -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<RegistryConfig>;
};
}
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<KernelOptions>;
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({

View File

@ -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 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;
}
}

View File

@ -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';

View File

@ -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>;
}