UNPKG

@liara/cli

Version:

The command line interface for Liara

316 lines (315 loc) 14.7 kB
var _AppLogs_instances, _a, _AppLogs_timestamps, _AppLogs_colorize, _AppLogs_releaseTagMap, _AppLogs_gray, _AppLogs_printLogLine; import { __classPrivateFieldGet, __classPrivateFieldSet } from "tslib"; import path from 'path'; import fs from 'fs-extra'; import chalk from 'chalk'; import moment from 'moment'; import UAParser from 'ua-parser-js'; import * as chrono from 'chrono-node'; import { Flags, Errors } from '@oclif/core'; import Command from '../../base.js'; import { createDebugLogger } from '../../utils/output.js'; import { BundlePlanError } from '../../errors/bundle-plan.js'; class AppLogs extends Command { constructor() { super(...arguments); _AppLogs_instances.add(this); _AppLogs_timestamps.set(this, false); _AppLogs_colorize.set(this, false); _AppLogs_releaseTagMap.set(this, new Map()); // cache to store releaseId with it's tag this.fetchLogs = async (since, appName, releaseId, last_lines = false) => { var _b; this.debug('Polling...'); try { const url = `v2/projects/${appName}/logs`; const searchParams = { start: since, direction: 'forward', last_lines: last_lines, }; if (releaseId) { searchParams.releaseId = releaseId; } const data = await this.got(url, { searchParams, timeout: { request: 20000, }, }).json(); // The data array can contain separated groups of log lines, lines within each object are sorted // But when flattening these lines, they are not sorted // We need to flatten all values first, then sort by timestamp if (!((_b = data === null || data === void 0 ? void 0 : data.data) === null || _b === void 0 ? void 0 : _b.length)) { return []; } // Fetch release tags for all releaseIds in the response const releaseIds = new Set(data.data.map((entry) => entry.metaData.releaseId)); for (const rid of releaseIds) { if (!__classPrivateFieldGet(this, _AppLogs_releaseTagMap, "f").has(rid)) { // This function isn’t very important — it’s just used as a safeguard and to ensure proper functionality const tag = await this.getReleaseTagFromId(appName, rid); if (tag) { __classPrivateFieldGet(this, _AppLogs_releaseTagMap, "f").set(rid, tag); } } } // Map each log line with its release tag const logsWithRelease = data.data.flatMap((entry) => (entry.values || []).map((value) => ({ values: value, releaseTag: __classPrivateFieldGet(this, _AppLogs_releaseTagMap, "f").get(entry.metaData.releaseId) || 'unknown', }))); // Sort all log lines by timestamp (first element of each [timestamp, logLine] tuple) logsWithRelease.sort((a, b) => { const timestampA = BigInt(a.values[0]); const timestampB = BigInt(b.values[0]); return timestampA < timestampB ? -1 : timestampA > timestampB ? 1 : 0; }); return logsWithRelease; } catch (error) { if (error.response && error.response.statusCode === 404) { // tslint:disable-next-line: no-console console.error(new Errors.CLIError('App not found.').render()); process.exit(2); } this.debug(error.response.body ? error.response.body : error.response); const genericErrorMessage = `'We encountered an issue and were unable to retrieve the logs. Solutions: 1) Check console logs from https://console.liara.ir/apps/${appName}/logs 2) Try ' liara logs -f --since="1 minute ago" ' command to see app logs. 3) Enable --debug for more details. 4) Try again later. `; console.error(new Errors.CLIError(genericErrorMessage).render()); process.exit(2); } }; this.fetchReleases = async (appName) => { this.debug('Fetching releases...'); try { const url = `v1/projects/${appName}/releases`; const data = await this.got(url, { searchParams: { page: 1, count: 100, // Get more releases to ensure we find the one user wants }, timeout: { request: 10000, }, }).json(); return data; } catch (error) { if (error.response && error.response.statusCode === 404) { console.error(new Errors.CLIError('App not found.').render()); process.exit(2); } this.debug(error.response.body ? error.response.body : error.response); console.error(new Errors.CLIError('Failed to fetch releases. Please try again later.').render()); process.exit(2); } }; this.getReleaseIdFromTag = async (appName, tag) => { this.debug(`Looking for release with tag: ${tag}`); const releasesData = await this.fetchReleases(appName); // Find the release with matching tag const release = releasesData.releases.find((r) => r.tag.toLowerCase() === tag.toLowerCase()); if (release) { this.debug(`Found release: ${release._id} for tag: ${tag}`); return release._id; } this.debug(`No release found for tag: ${tag}`); return undefined; }; this.getReleaseTagFromId = async (appName, releaseId) => { this.debug(`Looking for release tag with id: ${releaseId}`); const releasesData = await this.fetchReleases(appName); // Find the release with matching id const release = releasesData.releases.find((r) => r._id === releaseId); if (release) { this.debug(`Found tag: ${release.tag} for releaseId: ${releaseId}`); return release.tag; } this.debug(`No release found for id: ${releaseId}`); return undefined; }; } async run() { const { flags } = await this.parse(_a); const { follow, colorize, timestamps, release } = flags; const now = Math.floor(Date.now() / 1000); // current timestamp __classPrivateFieldSet(this, _AppLogs_timestamps, timestamps, "f"); __classPrivateFieldSet(this, _AppLogs_colorize, colorize, "f"); this.debug = createDebugLogger(flags.debug); await this.setGotConfig(flags); const projectConfig = this.readProjectConfig(process.cwd()); const project = flags.app || projectConfig.app || (await this.promptProject()); const { project: { planID, bundlePlanID, network }, } = await this.got(`v1/projects/${project}`).json(); // #OLD_INFRASTRUCTURE if (!network) { console.error('❌ This version of Liara CLI no longer supports apps running on the old infrastructure.\n' + '➡️ Please migrate your app to the new infrastructure or use an older version of the CLI.\n\n' + '🔧 To install the last supported version:\n' + ' $ npm i -g @liara/cli@8.1.0\n'); process.exit(1); } const { plans } = await this.got('v1/me').json(); const maxSince = now - plans.projectBundlePlans[planID][bundlePlanID].maxLogsRetention * 86400; let start = flags.since ? this.getStart(`${flags.since}`) : maxSince; if (start < maxSince) { console.error(new Errors.CLIError(BundlePlanError.max_logs_period(bundlePlanID)).render()); process.exit(2); } // Get releaseId if release flag is provided let releaseId; if (release) { releaseId = await this.getReleaseIdFromTag(project, release); if (!releaseId) { console.error(new Errors.CLIError(`Release "${release}" not found. Please check the release tag and try again.`).render()); process.exit(2); } else { __classPrivateFieldGet(this, _AppLogs_releaseTagMap, "f").set(releaseId, release); } this.debug(`Using releaseId: ${releaseId} for tag: ${release}`); } let lastLogUnix = 0; while (true) { const logs = await this.fetchLogs(Math.max(start, lastLogUnix), project, releaseId, flags['last-lines']); if (!(logs === null || logs === void 0 ? void 0 : logs.length) && !follow) { break; } for (const log of logs) { __classPrivateFieldGet(this, _AppLogs_instances, "m", _AppLogs_printLogLine).call(this, log.values, log.releaseTag); } if (flags['last-lines']) { break; } const lastLog = logs[logs.length - 1]; if (lastLog) { const unixTime = lastLog.values[0].slice(0, 10); lastLogUnix = parseInt(unixTime) + 1; } await this.sleep(1000); if (start) { start += 1; this.debug(`start timestamp: ${start}`); } } } async sleep(miliseconds) { return new Promise((resolve) => setTimeout(resolve, miliseconds)); } readProjectConfig(projectPath) { let content; const liaraJSONPath = path.join(projectPath, 'liara.json'); const hasLiaraJSONFile = fs.existsSync(liaraJSONPath); if (hasLiaraJSONFile) { try { content = fs.readJSONSync(liaraJSONPath) || {}; content.app && (content.app = content.app.toLowerCase()); } catch (error) { content = {}; this.error('Syntax error in `liara.json`!', error); } } return content || {}; } getStart(since) { const parsedDate = chrono.parseDate(`${since}`); const sinceUnix = moment(parsedDate).unix(); return sinceUnix; } } _a = AppLogs, _AppLogs_timestamps = new WeakMap(), _AppLogs_colorize = new WeakMap(), _AppLogs_releaseTagMap = new WeakMap(), _AppLogs_instances = new WeakSet(), _AppLogs_gray = function _AppLogs_gray(message) { if (!__classPrivateFieldGet(this, _AppLogs_colorize, "f")) return message; return chalk.gray(message); }, _AppLogs_printLogLine = function _AppLogs_printLogLine(log, releaseTag) { let message = JSON.parse(log[1])._entry; if (__classPrivateFieldGet(this, _AppLogs_colorize, "f")) { message = colorfulAccessLog(message); } if (__classPrivateFieldGet(this, _AppLogs_timestamps, "f")) { // iso string is docker's log format when using --timestamps const timestamp = __classPrivateFieldGet(this, _AppLogs_instances, "m", _AppLogs_gray).call(this, moment .unix(parseInt(log[0].substring(0, 10))) .format('YYYY-MM-DDTHH:mm:ss')); message = `${__classPrivateFieldGet(this, _AppLogs_instances, "m", _AppLogs_gray).call(this, releaseTag)} | ${timestamp} | ${message}`; } const socket = JSON.parse(log[1]).type === 'stderr' ? process.stderr : process.stdout; socket.write(message + '\n'); }; AppLogs.description = 'fetch the logs of an app'; AppLogs.flags = { ...Command.flags, app: Flags.string({ char: 'a', description: 'app id', parse: async (app) => app.toLowerCase(), }), since: Flags.string({ char: 's', description: 'show logs since a specific time in the past (e.g. "1 hour ago")', }), timestamps: Flags.boolean({ char: 't', description: 'show timestamps', default: false, }), follow: Flags.boolean({ char: 'f', description: 'follow log output', default: false, }), colorize: Flags.boolean({ char: 'c', description: 'colorize log output', default: false, }), release: Flags.string({ char: 'r', description: 'show logs for a specific release (e.g. v1, v2)', }), 'last-lines': Flags.boolean({ char: 'l', default: false, }), }; AppLogs.aliases = ['logs']; export default AppLogs; function colorfulAccessLog(message) { const COLOR_END = '\x1B[0m'; const CYAN = '\x1B[0;36m'; const GRAY = '\x1B[1;30m'; const MAGENTO = '\x1B[1;35m'; const GREEN = '\x1B[1;32m'; const RED = '\x1B[1;31m'; const YELLOW = '\x1B[1;33m'; const BLUE = '\x1B[1;34m'; return message .replace(/(((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})/, `${CYAN}$1${COLOR_END}`) .replace(/(GET|POST|PUT|DELETE|OPTIONS|HEAD) (401|402|403|404|409)/, `$1 ${MAGENTO}$2${COLOR_END}`) .replace(/(GET|POST|PUT|DELETE|OPTIONS|HEAD) (301|302|304)/, `$1 ${GRAY}$2${COLOR_END}`) .replace(/(GET|POST|PUT|DELETE|OPTIONS|HEAD) (200|201|204)/, `$1 ${GREEN}$2${COLOR_END}`) .replace(/(GET|POST|PUT|DELETE|OPTIONS|HEAD) (500|502|503|504)/, `$1 ${RED}$2${COLOR_END}`) .replace('GET', `${BLUE}GET${COLOR_END}`) .replace('POST', `${GREEN}POST${COLOR_END}`) .replace('PUT', `${GREEN}PUT${COLOR_END}`) .replace('DELETE', `${RED}DELETE${COLOR_END}`) .replace('OPTIONS', `${YELLOW}OPTIONS${COLOR_END}`) .replace('HEAD', `${YELLOW}HEAD${COLOR_END}`) .replace(/(\[error\].+), client:/, `${RED}$1${COLOR_END}, client:`) // Nginx error log .replace(/("Mozilla.+")/, (match) => { var matchWithoutColors = match.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); const { browser, os } = new UAParser(matchWithoutColors).getResult(); if (!browser.name || !os.name) { return `${GRAY}${matchWithoutColors}${COLOR_END}`; } return `${GRAY}"${browser.name} ${browser.version || ''} - ${os.name} ${os.version || ''}"${COLOR_END}`; }); }