arcadiasuite/server/kernel/core/LogAggregator.ts

222 lines
5.3 KiB
TypeScript

/**
* LogAggregator - Agregador de logs do Kernel
* Coleta, armazena e disponibiliza logs dos serviços
*/
import { EventEmitter } from 'events';
import { LogEntry, ServiceConfig } from '../types';
export interface LogAggregatorOptions {
maxLines?: number;
retentionHours?: number;
}
export class LogAggregator extends EventEmitter {
private logs: LogEntry[] = [];
private serviceBuffers: Map<string, LogEntry[]> = new Map();
private options: Required<LogAggregatorOptions>;
constructor(options: LogAggregatorOptions = {}) {
super();
this.options = {
maxLines: 10000,
retentionHours: 24,
...options,
};
// Limpa logs antigos periodicamente
setInterval(() => this.cleanup(), 60000);
}
/**
* Adiciona uma entrada de log
*/
add(entry: LogEntry): void {
// Adiciona ao buffer global
this.logs.push(entry);
// Adiciona ao buffer do serviço
if (!this.serviceBuffers.has(entry.serviceId)) {
this.serviceBuffers.set(entry.serviceId, []);
}
this.serviceBuffers.get(entry.serviceId)!.push(entry);
// Limita tamanho do buffer global
if (this.logs.length > this.options.maxLines) {
this.logs.shift();
}
// Limita tamanho do buffer por serviço
const serviceLogs = this.serviceBuffers.get(entry.serviceId)!;
if (serviceLogs.length > this.options.maxLines / 10) {
serviceLogs.shift();
}
this.emit('log', entry);
}
/**
* Adiciona log de sistema
*/
system(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
this.add({
timestamp: new Date(),
serviceId: 'kernel',
level,
message,
source: 'system',
});
}
/**
* Retorna todos os logs
*/
getAll(options: {
lines?: number;
since?: Date;
level?: 'info' | 'warn' | 'error' | 'debug';
serviceId?: string;
} = {}): LogEntry[] {
let logs = options.serviceId
? this.serviceBuffers.get(options.serviceId) || []
: [...this.logs];
if (options.since) {
logs = logs.filter(l => l.timestamp >= options.since!);
}
if (options.level) {
const levels = ['debug', 'info', 'warn', 'error'];
const minLevel = levels.indexOf(options.level);
logs = logs.filter(l => levels.indexOf(l.level) >= minLevel);
}
if (options.lines) {
logs = logs.slice(-options.lines);
}
return logs;
}
/**
* Retorna logs de um serviço específico
*/
getServiceLogs(serviceId: string, lines = 100): LogEntry[] {
const logs = this.serviceBuffers.get(serviceId) || [];
return logs.slice(-lines);
}
/**
* Retorna logs recentes (últimos N)
*/
getRecent(count = 50): LogEntry[] {
return this.logs.slice(-count);
}
/**
* Retorna logs como string formatada
*/
formatLogs(logs: LogEntry[]): string {
return logs.map(log => this.formatEntry(log)).join('\n');
}
/**
* Formata uma entrada de log
*/
formatEntry(entry: LogEntry): string {
const time = entry.timestamp.toISOString().split('T')[1].slice(0, 12);
const level = entry.level.toUpperCase().padStart(5);
const service = entry.serviceId.padEnd(20);
return `[${time}] [${level}] [${service}] ${entry.message}`;
}
/**
* Limpa logs de um serviço
*/
clearService(serviceId: string): void {
this.serviceBuffers.delete(serviceId);
this.logs = this.logs.filter(l => l.serviceId !== serviceId);
}
/**
* Limpa todos os logs
*/
clearAll(): void {
this.logs = [];
this.serviceBuffers.clear();
this.system('Logs limpos', 'info');
}
/**
* Retorna estatísticas de logs
*/
getStats(): {
totalLogs: number;
logsByService: Record<string, number>;
logsByLevel: Record<string, number>;
} {
const logsByService: Record<string, number> = {};
const logsByLevel: Record<string, number> = {};
for (const log of this.logs) {
logsByService[log.serviceId] = (logsByService[log.serviceId] || 0) + 1;
logsByLevel[log.level] = (logsByLevel[log.level] || 0) + 1;
}
return {
totalLogs: this.logs.length,
logsByService,
logsByLevel,
};
}
/**
* Stream de logs em tempo real (retorna handler para unsubscribe)
*/
stream(callback: (entry: LogEntry) => void, options: {
serviceId?: string;
level?: 'info' | 'warn' | 'error' | 'debug';
} = {}): () => void {
const handler = (entry: LogEntry) => {
if (options.serviceId && entry.serviceId !== options.serviceId) {
return;
}
if (options.level) {
const levels = ['debug', 'info', 'warn', 'error'];
if (levels.indexOf(entry.level) < levels.indexOf(options.level)) {
return;
}
}
callback(entry);
};
this.on('log', handler);
return () => {
this.off('log', handler);
};
}
/**
* Limpa logs antigos
*/
private cleanup(): void {
const cutoff = new Date();
cutoff.setHours(cutoff.getHours() - this.options.retentionHours);
const initialCount = this.logs.length;
this.logs = this.logs.filter(l => l.timestamp >= cutoff);
for (const [serviceId, logs] of this.serviceBuffers) {
this.serviceBuffers.set(serviceId, logs.filter(l => l.timestamp >= cutoff));
}
const removed = initialCount - this.logs.length;
if (removed > 0) {
this.system(`Cleanup: ${removed} logs antigos removidos`, 'debug');
}
}
}