UNPKG

@swell/cli

Version:

Swell's command line interface/utility

212 lines (211 loc) 7.67 kB
import { Flags } from '@oclif/core'; import ora from 'ora'; import { LineOutput, LoggedItem, TableOutput } from '../lib/logs/index.js'; import { SwellCommand } from '../swell-command.js'; // the columns available to display in the table const OUTPUT_COLUMNS = [ 'app', 'data', 'date', 'env', 'ip', 'request', 'req', 'status', 'time', 'type', ]; // the default columns to display in the table const OUTPUT_COLUMNS_DEFAULTS = ['date', 'request', 'data', 'status', 'time']; // the interval to poll the API when following logs const FOLLOW_POLLING_INTERVAL = 1000 * 2; // 2 seconds /** * Builds the request options to send to the API based on the flags passed * to the command. * * @param flags the flags passed to the command * @returns the request options to send to the API */ function buildLogRequestBody(flags) { const reqOptions = { where: {} }; const andConditions = []; reqOptions.limit = flags.number; if (flags.search) { reqOptions.search = flags.search; } // some filters are simple and can be mapped directly to the API const logFilters = [ { filter: 'app_id', flag: 'app', operator: '$in' }, { filter: 'message.status', flag: 'status', operator: '$in' }, { filter: 'message.type', flag: 'type', operator: '$in' }, ]; // map the flags to the API query for (const filter of logFilters) { if (flags[filter.flag]) { andConditions.push({ [filter.filter]: filter.operator ? { [filter.operator]: flags[filter.flag] } : flags[filter.flag], }); } } const { after, before, env, startPolling } = flags; if (env) { andConditions.push({ environment_id: env }); } else { // Default to test env andConditions.push({ environment_id: 'test' }); } // if we are polling, we want to get logs after the last date we received if (startPolling) { andConditions.push({ date: { $gt: startPolling } }); } else { if (after) { if (typeof after === 'object' && after !== null) { andConditions.push({ date: after }); } else { andConditions.push({ date: { $gte: after } }); } } if (before) { andConditions.push({ date: { $lte: before } }); } } // if there are any conditions, add them to the request options // otherwise, the server will complain for an empty $and array if (andConditions.length > 0) { reqOptions.where.$and = andConditions; } return reqOptions; } export default class Logs extends SwellCommand { static examples = [ 'swell logs', 'swell logs -f -p -n 10 -c date,req,data', 'swell logs -s accounts', 'swell logs --after 2023-08-25T16:01:02.697Z -c date', 'swell logs --after 2023-08-27', ]; static flags = { after: Flags.string({ description: 'expected is Javascript Date Time String Format: YYYY-MM-DDTHH:mm:ss.sssZ', parse: async (value) => new Date(value).toISOString(), summary: 'show logs created on or after this date/time (utc)', }), // filters app: Flags.string({ description: 'filter logs by app id', }), before: Flags.string({ description: 'expected is Javascript Date Time String Format: YYYY-MM-DDTHH:mm:ss.sssZ', parse: async (value) => new Date(value).toISOString(), summary: 'show logs created on or before this date/time (utc)', }), columns: Flags.string({ char: 'c', default: OUTPUT_COLUMNS_DEFAULTS.join(','), description: `available columns: ${OUTPUT_COLUMNS.join(', ')}`, // validate the columns requested for display async parse(value) { const flagColumns = value.split(/\s*,\s*/g); for (const column of flagColumns) { if (!OUTPUT_COLUMNS.includes(column)) { throw new Error(`Invalid column: ${column}`); } } return flagColumns.join(','); }, summary: 'comma separated list of columns to display', }), env: Flags.string({ description: 'filter logs by environment id', }), // control / output follow: Flags.boolean({ char: 'f', default: false, description: 'stream logs in real time', exclusive: ['before'], }), number: Flags.integer({ char: 'n', default: 100, description: 'number of log lines to return', }), output: Flags.string({ char: 'o', default: 'line', description: 'how to display the logs', options: ['table', 'line'], }), pretty: Flags.boolean({ char: 'p', default: false, description: 'note that this flag will take more space in the terminal and require more time to load', summary: 'pretty print json data', }), search: Flags.string({ char: 's', description: 'search logs by keyword', }), status: Flags.string({ description: 'filter logs by request status', }), type: Flags.string({ description: 'filter logs by type', multiple: true, options: ['api', 'function', 'webhook'], }), }; static summary = 'Output or stream store logs to the terminal.'; output; async run() { const spinner = ora(); spinner.fail('This command is temporarily disabled. Check back soon.'); } async __run() { const { flags } = await this.parse(Logs); // indentify the columns to display const columns = flags.columns.split(','); // create the output stream this.output = flags.output === 'table' ? new TableOutput(columns, flags.pretty) : new LineOutput(columns, flags.pretty); // first run for getting the logs // we keep track of the last date we received so we can get logs after that flags.startPolling = await this.getLogsAndWriteToStream(flags); // if following, poll the API every FOLLOW_POLLING_INTERVAL seconds and // write new logs to the stream if (flags.follow) { setInterval(async () => { // when polling, we want to get logs after the last date we received flags.startPolling = await this.getLogsAndWriteToStream(flags); }, FOLLOW_POLLING_INTERVAL); } } async getLogs(flags) { const body = buildLogRequestBody(flags); const response = await this.api.post({ adminPath: `/data/$get/:logs` }, { body }); return response?.results?.reverse() || []; } async getLogsAndWriteToStream(flags) { const logs = await this.getLogs(flags); let lastDate = ''; if (!this.output) { this.error('Output stream not available'); } if (logs.length > 0) { lastDate = logs.at(-1).date; for (const log of logs) { this.output.write(this.output.prepareData(new LoggedItem(log))); } } // if the user is following logs, we want to return the last date we // received return lastDate || flags.startPolling; } }