import chalk from 'chalk'; import { RootAPIHelper, getNewApiHelper } from '../helpers/root-api'; import { CollectionModuleConfig } from '../domain/collection-modules/collection-module-config'; import { NetworkCmLogItem, CmLogItem, cmLogItemFromNetwork } from '../domain/collection-modules/cm-log-item'; import { parseTimeDuration } from '../helpers/parse-time-duration'; import { symbols } from '../helpers/symbols'; import { runWithSpinner } from '../helpers/spinner'; interface CmLogsOptions { time?: string; level?: string; policyId?: string; functionName?: string; watch?: boolean; } interface CmLogsParams extends CmLogsOptions { apiKey: string; config: CollectionModuleConfig; } interface FetchLogsParams { rootApi: RootAPIHelper; queryParams: Record; hasFilters: boolean; count: number; } const formatLogLevel = (level: string) => { switch (level) { case 'error': { return chalk.red.bold(`[${level.toUpperCase()}]`); } case 'warn': { return chalk.yellow.bold(`[${level.toUpperCase()}]`); } case 'debug': { return chalk.magenta(`[${level.toUpperCase()}]`); } case 'info': { return chalk.blue(`[${level.toUpperCase()}]`); } default: { return chalk.white(`[${level.toUpperCase()}]`); } } }; const formatTimestamp = (timestamp: string): string => { const date = new Date(timestamp); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / 60_000); const diffHours = Math.floor(diffMs / 3_600_000); const diffDays = Math.floor(diffMs / 86_400_000); let relative = ''; if (diffMins < 1) { relative = 'just now'; } else if (diffMins < 60) { relative = `${diffMins}m ago`; } else if (diffHours < 24) { relative = `${diffHours}h ago`; } else { relative = `${diffDays}d ago`; } // Format as HH:MM:SS const timeStr = date.toISOString().split('T')[1].split('.')[0]; const relativeStr = chalk.gray(`(${relative})`); return `${chalk.gray(timeStr)} ${relativeStr}`; }; const formatMessage = (message: string): string => { // Try to detect and pretty-print JSON if (message.includes('{') && message.includes('}')) { const parts = message.split(/(\{[^{}]*\})/); return parts .map((part) => { if (part.startsWith('{') && part.endsWith('}')) { try { const obj = JSON.parse(part); const pretty = JSON.stringify(obj, null, 2); return chalk.gray('\n') + chalk.dim(pretty); } catch { return part; } } return part; }) .join(''); } // Return full message without truncation return message; }; const printLogs = (logs: CmLogItem[]) => { // Sort logs chronologically (earliest to latest) const sortedLogs = [...logs].sort((a, b) => { return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(); }); console.log(); // Empty line before logs for (const log of sortedLogs) { const { timestamp, level, functionName, message, policyId } = log; // Main log line console.log(`${formatTimestamp(timestamp)} ${formatLogLevel(level)} ${chalk.cyan(functionName)}`); // Policy ID if available if (policyId) { console.log(` ${chalk.gray('Policy:')} ${chalk.white(policyId)}`); } // Message (indented) const formattedMessage = formatMessage(message); formattedMessage.split('\n').forEach((line) => { console.log(` ${line}`); }); // Separator between log entries console.log(); } }; const fetchLogs = async (params: FetchLogsParams): Promise => { const { rootApi, queryParams } = params; try { const response = await rootApi.send({ path: '/insurance/collection-modules/logs', searchParams: queryParams, }); const networkLogs = response as NetworkCmLogItem[]; return networkLogs.map(cmLogItemFromNetwork); } catch (error: any) { if (error.message?.includes('not enabled') || error.message?.includes('feature flag')) { throw new Error( 'Collection module DynamoDB logs are not enabled for this organization. Please contact support to enable this feature.', ); } throw error; } }; const sleep = (ms: number): Promise => { return new Promise((resolve) => setTimeout(resolve, ms)); }; export const cmLogs = async (params: CmLogsParams) => { const { apiKey, config, time, level, policyId, functionName, watch } = params; // Parse count - use higher default when filters are applied since they work post-query const hasFilters = !!(level || functionName || time); const count = hasFilters ? 500 : 100; // Build query parameters const queryParams: Record = { limit: count.toString(), }; // Convert time duration to from_date ISO string if (time) { try { const fromDate = parseTimeDuration(time); queryParams.from_date = fromDate; } catch (error: any) { throw new Error(`Invalid time duration: ${String(error.message)}`); } } // Add optional filters if (policyId) { queryParams.policy_id = policyId; } if (level) { queryParams.level = level; } if (functionName) { queryParams.function_name = functionName; } // Create API helper const { host } = config; const rootApi: RootAPIHelper = getNewApiHelper({ host, apiKey, throwResponseErrors: true }); // Watch mode: continuously poll for new logs if (watch) { console.log(chalk.blue(`\n${symbols.retry}Watching for new logs... (Press Ctrl+C to stop)\n`)); const seenLogIds = new Set(); let isFirstFetch = true; // Handle Ctrl+C gracefully process.once('SIGINT', () => { console.log(chalk.gray(`\n\n${symbols.success}Stopped watching logs`)); process.exit(0); }); while (true) { try { const logs = await fetchLogs({ rootApi, queryParams, hasFilters, count }); const newLogs = logs.filter((log) => { return !seenLogIds.has(log.logId); }); if (newLogs.length > 0) { if (!isFirstFetch) { console.log(); } printLogs(newLogs); for (const log of newLogs) { seenLogIds.add(log.logId); } } isFirstFetch = false; // Wait before next poll (3 seconds) await sleep(3000); } catch (error: any) { console.error(chalk.red(`\n${symbols.error}Error fetching logs:`), error.message); console.log(chalk.gray('Retrying in 5 seconds...')); await sleep(5000); } } } // Normal mode: fetch once and display const logs = await runWithSpinner('Fetching collection module logs...', () => fetchLogs({ rootApi, queryParams, hasFilters, count }), ); // Display logs if (logs.length === 0) { console.log(`\n${symbols.info}No collection module logs found.`); if (hasFilters && !policyId) { console.log(chalk.gray('\nTips:')); console.log(chalk.gray(' • Try without filters: rp logs')); console.log(chalk.gray(' • Add --policy-id for faster queries')); } return; } printLogs(logs); // Show summary console.log(chalk.gray(`${logs.length} log ${logs.length === 1 ? 'entry' : 'entries'}`)); };