Files
dcrouter/ts/opsserver/handlers/logs.handler.ts

250 lines
7.7 KiB
TypeScript

import * as plugins from '../../plugins.js';
import type { OpsServer } from '../classes.opsserver.js';
import * as interfaces from '../../../ts_interfaces/index.js';
import { logBuffer } from '../../logger.js';
export class LogsHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
constructor(private opsServerRef: OpsServer) {
// Add this handler's router to the parent
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers();
}
private registerHandlers(): void {
// Get Recent Logs Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>(
'getRecentLogs',
async (dataArg, toolsArg) => {
const logs = await this.getRecentLogs(
dataArg.level,
dataArg.category,
dataArg.limit || 100,
dataArg.offset || 0,
dataArg.search,
dataArg.timeRange
);
return {
logs,
total: logs.length, // TODO: Implement proper total count
hasMore: false, // TODO: Implement proper pagination
};
}
)
);
// Get Log Stream Handler
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
'getLogStream',
async (dataArg, toolsArg) => {
// Create a virtual stream for log streaming
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
// Set up log streaming
const streamLogs = this.setupLogStream(
virtualStream,
dataArg.filters?.level,
dataArg.filters?.category,
dataArg.follow
);
// Start streaming
streamLogs.start();
// VirtualStream handles cleanup automatically
return {
logStream: virtualStream as any, // Cast to IVirtualStream interface
};
}
)
);
}
private static mapLogLevel(smartlogLevel: string): 'debug' | 'info' | 'warn' | 'error' {
switch (smartlogLevel) {
case 'silly':
case 'debug':
return 'debug';
case 'warn':
return 'warn';
case 'error':
return 'error';
default:
return 'info';
}
}
private static deriveCategory(
zone?: string,
message?: string
): 'smtp' | 'dns' | 'security' | 'system' | 'email' {
const msg = (message || '').toLowerCase();
if (msg.includes('[security:') || msg.includes('security')) return 'security';
if (zone === 'email' || msg.includes('email') || msg.includes('smtp') || msg.includes('mta')) return 'email';
if (zone === 'dns' || msg.includes('dns')) return 'dns';
if (msg.includes('smtp')) return 'smtp';
return 'system';
}
private async getRecentLogs(
level?: 'error' | 'warn' | 'info' | 'debug',
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email',
limit: number = 100,
offset: number = 0,
search?: string,
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'
): Promise<Array<{
timestamp: number;
level: 'debug' | 'info' | 'warn' | 'error';
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
message: string;
metadata?: any;
}>> {
// Compute a timestamp cutoff from timeRange
let since: number | undefined;
if (timeRange) {
const rangeMs: Record<string, number> = {
'1h': 3600000,
'6h': 21600000,
'24h': 86400000,
'7d': 604800000,
'30d': 2592000000,
};
since = Date.now() - (rangeMs[timeRange] || 86400000);
}
// Map the UI level to smartlog levels for filtering
const smartlogLevels: string[] | undefined = level
? level === 'debug'
? ['debug', 'silly']
: level === 'info'
? ['info', 'ok', 'success', 'note', 'lifecycle']
: [level]
: undefined;
// Fetch a larger batch from buffer, then apply category filter client-side
const rawEntries = logBuffer.getEntries({
level: smartlogLevels as any,
search,
since,
limit: limit * 3, // over-fetch to compensate for category filtering
offset: 0,
});
// Map ILogPackage → UI log format and apply category filter
const mapped: Array<{
timestamp: number;
level: 'debug' | 'info' | 'warn' | 'error';
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
message: string;
metadata?: any;
}> = [];
for (const pkg of rawEntries) {
const uiLevel = LogsHandler.mapLogLevel(pkg.level);
const uiCategory = LogsHandler.deriveCategory(pkg.context?.zone, pkg.message);
if (category && uiCategory !== category) continue;
mapped.push({
timestamp: pkg.timestamp,
level: uiLevel,
category: uiCategory,
message: pkg.message,
metadata: pkg.data,
});
if (mapped.length >= limit) break;
}
return mapped;
}
private setupLogStream(
virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>,
levelFilter?: string[],
categoryFilter?: string[],
follow: boolean = true
): {
start: () => void;
stop: () => void;
} {
let intervalId: NodeJS.Timeout | null = null;
let logIndex = 0;
const start = () => {
if (!follow) {
// Send existing logs and close
this.getRecentLogs(
levelFilter?.[0] as any,
categoryFilter?.[0] as any,
100,
0
).then(logs => {
logs.forEach(log => {
const logData = JSON.stringify(log);
const encoder = new TextEncoder();
virtualStream.sendData(encoder.encode(logData));
});
// VirtualStream doesn't have end() method - it closes automatically
});
return;
}
// For follow mode, simulate real-time log streaming
intervalId = setInterval(async () => {
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
// Filter by requested criteria
if (levelFilter && !levelFilter.includes(mockLevel)) return;
if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
const logEntry = {
timestamp: Date.now(),
level: mockLevel,
category: mockCategory,
message: `Real-time log ${logIndex++} from ${mockCategory}`,
metadata: {
requestId: plugins.uuid.v4(),
},
};
const logData = JSON.stringify(logEntry);
const encoder = new TextEncoder();
try {
await virtualStream.sendData(encoder.encode(logData));
} catch {
// Stream closed or errored — clean up to prevent interval leak
clearInterval(intervalId!);
intervalId = null;
}
}, 2000); // Send a log every 2 seconds
// TODO: Hook into actual logger events
// logger.on('log', (logEntry) => {
// if (matchesCriteria(logEntry, level, service)) {
// virtualStream.sendData(formatLogEntry(logEntry));
// }
// });
};
const stop = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
// TODO: Unhook from logger events
};
return { start, stop };
}
}