@swell/cli
Version:
Swell's command line interface/utility
212 lines (211 loc) • 7.67 kB
JavaScript
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.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;
}
}