181 lines
7.1 KiB
TypeScript
181 lines
7.1 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
|
import type { CliArguments } from '../../types.js';
|
|
import { registerIpcCommand } from '../../registration/index.js';
|
|
import { getBool, getNumber, getString } from '../../helpers/argv.js';
|
|
import { formatLog } from '../../helpers/formatting.js';
|
|
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
|
|
|
|
export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|
registerIpcCommand(
|
|
smartcli,
|
|
'logs',
|
|
async (argvArg: CliArguments) => {
|
|
const target = argvArg._[1];
|
|
if (!target) {
|
|
console.error('Error: Please provide a process target');
|
|
console.log('Usage: tspm logs <id | id:N | name:LABEL> [options]');
|
|
console.log('\nOptions:');
|
|
console.log(' --lines <n> Number of lines to show (default: 50)');
|
|
console.log(' --since <dur> Only show logs since duration (e.g., 10m, 2h, 1d)');
|
|
console.log(' --stderr-only Only show stderr logs');
|
|
console.log(' --stdout-only Only show stdout logs');
|
|
console.log(' --ndjson Output each log as JSON line');
|
|
console.log(' --follow Stream logs in real-time (like tail -f)');
|
|
return;
|
|
}
|
|
|
|
const lines = getNumber(argvArg, 'lines', 50);
|
|
const follow = getBool(argvArg, 'follow', 'f');
|
|
const sinceSpec = getString(argvArg, 'since');
|
|
const stderrOnly = getBool(argvArg, 'stderr-only');
|
|
const stdoutOnly = getBool(argvArg, 'stdout-only');
|
|
const ndjson = getBool(argvArg, 'ndjson');
|
|
|
|
const parseDuration = (spec?: string): number | undefined => {
|
|
if (!spec) return undefined;
|
|
const m = spec.trim().match(/^(\d+)(ms|s|m|h|d)?$/i);
|
|
if (!m) return undefined;
|
|
const val = Number(m[1]);
|
|
const unit = (m[2] || 'm').toLowerCase();
|
|
const mult = unit === 'ms' ? 1 : unit === 's' ? 1000 : unit === 'm' ? 60000 : unit === 'h' ? 3600000 : 86400000;
|
|
return Date.now() - val * mult;
|
|
};
|
|
const sinceTime = parseDuration(sinceSpec);
|
|
const typesFilter: Array<'stdout' | 'stderr' | 'system'> | undefined =
|
|
stderrOnly && !stdoutOnly
|
|
? ['stderr']
|
|
: stdoutOnly && !stderrOnly
|
|
? ['stdout']
|
|
: undefined; // all
|
|
|
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
|
const id = resolved.id;
|
|
const response = await tspmIpcClient.request('getLogs', { id, lines: sinceTime ? 0 : lines });
|
|
|
|
if (!follow) {
|
|
// One-shot mode - auto-disconnect handled by registerIpcCommand
|
|
const filtered = response.logs.filter((l) => {
|
|
if (typesFilter && !typesFilter.includes(l.type)) return false;
|
|
if (sinceTime && new Date(l.timestamp).getTime() < sinceTime) return false;
|
|
return true;
|
|
});
|
|
console.log(`Logs for process: ${id} (${sinceTime ? 'since ' + new Date(sinceTime).toLocaleString() : 'last ' + lines + ' lines'})`);
|
|
console.log('─'.repeat(60));
|
|
for (const log of filtered) {
|
|
if (ndjson) {
|
|
console.log(
|
|
JSON.stringify({
|
|
...log,
|
|
timestamp: new Date(log.timestamp).getTime(),
|
|
}),
|
|
);
|
|
} else {
|
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
|
const prefix =
|
|
log.type === 'stdout'
|
|
? '[OUT]'
|
|
: log.type === 'stderr'
|
|
? '[ERR]'
|
|
: '[SYS]';
|
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Streaming mode
|
|
console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
|
|
console.log('─'.repeat(60));
|
|
|
|
// Prepare backlog printing state and stream handler
|
|
let lastSeq = 0;
|
|
let lastRunId: string | undefined = undefined;
|
|
const printLog = (log: any) => {
|
|
if (typesFilter && !typesFilter.includes(log.type)) return;
|
|
if (sinceTime && new Date(log.timestamp).getTime() < sinceTime) return;
|
|
if (ndjson) {
|
|
console.log(
|
|
JSON.stringify({
|
|
...log,
|
|
timestamp: new Date(log.timestamp).getTime(),
|
|
}),
|
|
);
|
|
} else {
|
|
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
|
const prefix =
|
|
log.type === 'stdout'
|
|
? '[OUT]'
|
|
: log.type === 'stderr'
|
|
? '[ERR]'
|
|
: '[SYS]';
|
|
console.log(`${timestamp} ${prefix} ${log.message}`);
|
|
}
|
|
};
|
|
|
|
// Print initial backlog (already fetched via getLogs)
|
|
for (const log of response.logs) {
|
|
printLog(log);
|
|
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
|
|
if ((log as any).runId) lastRunId = (log as any).runId;
|
|
}
|
|
|
|
// Request additional backlog delivered as incremental messages to avoid large payloads
|
|
try {
|
|
const disposeBacklog = tspmIpcClient.onBacklogTopic(id, (log: any) => {
|
|
if (log.runId && log.runId !== lastRunId) {
|
|
console.log(`[INFO] Detected process restart (runId changed).`);
|
|
lastSeq = -1;
|
|
lastRunId = log.runId;
|
|
}
|
|
if (log.seq !== undefined && log.seq <= lastSeq) return;
|
|
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
|
console.log(
|
|
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
|
|
);
|
|
}
|
|
printLog({ ...log, timestamp: new Date(log.timestamp) });
|
|
if (log.seq !== undefined) lastSeq = log.seq;
|
|
});
|
|
await tspmIpcClient.requestLogsBacklogStream(id, { lines: sinceTime ? undefined : lines, sinceTime, types: typesFilter });
|
|
// Dispose backlog handler after a short grace (backlog is finite)
|
|
setTimeout(() => disposeBacklog(), 10000);
|
|
} catch {}
|
|
|
|
await withStreamingLifecycle(
|
|
async () => {
|
|
await tspmIpcClient.subscribe(id, (log: any) => {
|
|
// Reset sequence if runId changed (e.g., process restarted)
|
|
if (log.runId && log.runId !== lastRunId) {
|
|
console.log(`[INFO] Detected process restart (runId changed).`);
|
|
lastSeq = -1;
|
|
lastRunId = log.runId;
|
|
}
|
|
if (log.seq !== undefined && log.seq <= lastSeq) return;
|
|
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
|
console.log(
|
|
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
|
|
);
|
|
}
|
|
printLog(log);
|
|
if (log.seq !== undefined) lastSeq = log.seq;
|
|
});
|
|
},
|
|
async () => {
|
|
console.log('\n\nStopping log stream...');
|
|
try {
|
|
await tspmIpcClient.unsubscribe(id);
|
|
} catch {}
|
|
try {
|
|
await tspmIpcClient.disconnect();
|
|
} catch {}
|
|
},
|
|
);
|
|
},
|
|
{
|
|
actionLabel: 'get logs',
|
|
keepAlive: (argv) => getBool(argv, 'follow', 'f'),
|
|
},
|
|
);
|
|
}
|