UNPKG

clever-tools

Version:

Command Line Interface for Clever Cloud.

179 lines (154 loc) 6 kB
import { ApplicationLogStream } from '@clevercloud/client/esm/streams/application-logs.js'; import { styleText } from '../lib/style-text.js'; import { Logger } from '../logger.js'; import { conf } from './configuration.js'; import { waitForDeploymentEnd, waitForDeploymentStart } from './deployments.js'; import { getBest } from './domain.js'; import * as ExitStrategy from './exit-strategy-option.js'; import { JsonArray } from './json-array.js'; import { getHostAndTokens, processError } from './send-to-api.js'; import { Deferred } from './utils.js'; const RESET_COLOR = '\x1B[0m'; // 2000 logs per 100ms maximum const THROTTLE_ELEMENTS = 2000; const THROTTLE_PER_IN_MILLISECONDS = 100; const retryConfiguration = { enabled: true, initRetryTimeout: 3000, maxRetryCount: 10, }; export async function displayLogs(params) { const deferred = params.deferred || new Deferred(); const { apiHost, tokens } = await getHostAndTokens(); const { ownerId, appId, filter, since, until, deploymentId, format } = params; if (format === 'json' && until == null) { throw new Error('"json" format is only applicable with a limiting parameter such as `--until`'); } const logStream = new ApplicationLogStream({ apiHost, tokens, ownerId, appId, connectionTimeout: 10_000, retryConfiguration, since, until, deploymentId, filter, throttleElements: THROTTLE_ELEMENTS, throttlePerInMilliseconds: THROTTLE_PER_IN_MILLISECONDS, }); // Properly close the stream process.once('SIGINT', (signal) => logStream.close(signal)); const jsonArray = new JsonArray(); logStream .on('open', () => { Logger.debug(styleText('blue', `Logs stream (open) ${JSON.stringify({ appId, filter, deploymentId })}`)); if (format === 'json') { jsonArray.open(); } }) .on('error', (event) => { Logger.debug(styleText('red', `Logs stream (error) ${event.error.message}`)); }) .onLog((log) => { switch (format) { case 'json': jsonArray.push(log); return; case 'json-stream': Logger.printJson(log); return; case 'human': default: if (log.message === RESET_COLOR) { return; } Logger.println(formatLogLine(log)); } }); // start() is blocking until end of stream logStream .start() .then(() => { if (format === 'json') { jsonArray.close(); } return deferred.resolve(); }) .catch(processError) .catch((error) => deferred.reject(error)); return logStream; } export async function watchDeploymentAndDisplayLogs(options) { const { ownerId, appId, deploymentId, commitId, knownDeployments, quiet, redeployDate, exitStrategy } = options; ExitStrategy.plotQuietWarning(exitStrategy, quiet); // If in quiet mode, we only log start/finished deployment messages if (!quiet) { Logger.println(` ${styleText('blue', '→ Waiting for deployment to start…')}`); } const deployment = await waitForDeploymentStart({ ownerId, appId, deploymentId, commitId, knownDeployments }); Logger.println(` ${styleText('green', `✓ Deployment started ${styleText('grey', `(${deployment.uuid})`)}`)}`); if (exitStrategy === 'deploy-start') { return; } const deferred = new Deferred(); let logsStream; if (!quiet) { // About the deferred… // If displayLogs() throws an error, // the async function we're in (watchDeploymentAndDisplayLogs) will stop here and the error will be passed to the parent. // displayLogs() defines callback listeners so if it catches error in those callbacks, // it has no proper way to bubble up the error here. // Using the deferred enables this. logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred }); } if (!quiet) { Logger.println(` ${styleText('blue', '→ Waiting for application logs…')}`); } // Wait for deployment end (or an error thrown by logs with the deferred) const deploymentEnded = await Promise.race([ waitForDeploymentEnd({ ownerId, appId, deploymentId: deployment.uuid }), deferred.promise, ]); if (!quiet && exitStrategy !== 'never') { logsStream.close(quiet ? 'quiet' : 'follow'); } if (deploymentEnded.state === 'OK') { const favouriteDomain = await getBest(appId, ownerId); Logger.println(''); Logger.println( `${styleText(['bold', 'green'], '✓ Access your application:')} ${styleText(['underline', 'bold'], `https://${favouriteDomain.fqdn}`)}`, ); Logger.println( `${styleText(['bold', 'blue'], '→ Manage your application:')} ${styleText(['underline', 'bold'], `${conf.GOTO_URL}/${appId}`)}`, ); } else if (deploymentEnded.state === 'CANCELLED') { throw new Error('Deployment was cancelled. Please check the activity'); } else { throw new Error('Deployment failed. Please check the logs'); } } function formatLogLine(log) { const { date, message } = log; if (isDeploymentSuccessMessage(log)) { return `${date.toISOString()}: ${styleText(['bold', 'green'], message)}`; } else if (isDeploymentFailedMessage(log)) { return `${date.toISOString()}: ${styleText(['bold', 'red'], message)}`; } else if (isBuildSucessMessage(log)) { return `${date.toISOString()}: ${styleText(['bold', 'blue'], message)}`; } return `${date.toISOString()}: ${message}${RESET_COLOR}`; } function isCleverMessage(log) { return log.service !== 'bas-deploy.service'; } function isDeploymentSuccessMessage(log) { return isCleverMessage(log) && log.message.toLowerCase().startsWith('successfully deployed in'); } function isDeploymentFailedMessage(log) { return isCleverMessage(log) && log.message.toLowerCase().startsWith('deploy failed in'); } function isBuildSucessMessage(log) { return isCleverMessage(log) && log.message.toLowerCase().startsWith('build succeeded in'); }