feat(daemon): Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
This commit is contained in:
78
ts/cli.ts
78
ts/cli.ts
@@ -426,20 +426,84 @@ export const run = async (): Promise<void> => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm logs <id>');
|
||||
console.log('Usage: tspm logs <id> [options]');
|
||||
console.log('\nOptions:');
|
||||
console.log(' --lines <n> Number of lines to show (default: 50)');
|
||||
console.log(' --follow Stream logs in real-time (like tail -f)');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = argvArg.lines || 50;
|
||||
const follow = argvArg.follow || argvArg.f || false;
|
||||
|
||||
// Get initial logs
|
||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||
|
||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||
console.log('─'.repeat(60));
|
||||
if (!follow) {
|
||||
// Static log output
|
||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
for (const log of response.logs) {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : '[ERR]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
for (const log of response.logs) {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
}
|
||||
} else {
|
||||
// Streaming log output
|
||||
console.log(`Logs for process: ${id} (streaming...)`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
// Display initial logs
|
||||
let lastSeq = 0;
|
||||
for (const log of response.logs) {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
if (log.seq !== undefined) {
|
||||
lastSeq = Math.max(lastSeq, log.seq);
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to real-time updates
|
||||
await tspmIpcClient.subscribe(id, (log: any) => {
|
||||
// Check for sequence gap or duplicate
|
||||
if (log.seq !== undefined && log.seq <= lastSeq) {
|
||||
return; // Skip duplicate
|
||||
}
|
||||
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
||||
console.log(`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`);
|
||||
}
|
||||
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
|
||||
if (log.seq !== undefined) {
|
||||
lastSeq = log.seq;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Ctrl+C gracefully
|
||||
let isCleaningUp = false;
|
||||
const cleanup = async () => {
|
||||
if (isCleaningUp) return;
|
||||
isCleaningUp = true;
|
||||
console.log('\n\nStopping log stream...');
|
||||
try {
|
||||
await tspmIpcClient.unsubscribe(id);
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch (err) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', cleanup);
|
||||
process.on('SIGTERM', cleanup);
|
||||
|
||||
// Keep the process alive
|
||||
await new Promise(() => {}); // Block forever until interrupted
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting logs:', error.message);
|
||||
|
Reference in New Issue
Block a user