'use strict'; const { writeText, style, log, progress } = require('@serverless/utils/log'); const { frontend } = require('@serverless/utils/lib/auth/urls'); const colorize = require('json-colorizer'); const WebSocket = require('ws'); const chalk = require('chalk'); const { devModeFeed } = require('@serverless/utils/lib/auth/urls'); const consoleUi = require('@serverless/utils/console-ui'); const streamBuffers = require('stream-buffers'); const apiRequest = require('@serverless/utils/api-request'); const promptWithHistory = require('@serverless/utils/inquirer/prompt-with-history'); const streamBuff = new streamBuffers.ReadableStreamBuffer({ frequency: 500, chunkSize: 2048 * 1000000, }); const consoleMonitoringCounter = { logBatches: 0, events: 0, responses: 0, }; const jsonError = { colors: { BRACE: '#FD5750', BRACKET: '#FD5750', COLON: '#FD5750', COMMA: '#FD5750', STRING_KEY: '#FD5750', STRING_LITERAL: '#FD5750', NUMBER_LITERAL: '#FD5750', BOOLEAN_LITERAL: '#FD5750', NULL_LITERAL: '#FD5750', }, }; const jsonColors = { colors: { BRACE: 'white', BRACKET: 'white', COLON: 'white.bold', COMMA: 'white', STRING_KEY: 'white.bold', STRING_LITERAL: 'white', NUMBER_LITERAL: 'white', BOOLEAN_LITERAL: 'white', NULL_LITERAL: 'white', }, }; const headerChalk = chalk.grey; const errorTracker = {}; const handleSocketMessage = (context) => (data) => { try { const verbose = context.options.verbose; const splitData = data.toString('utf-8').split(';;;'); const jsonArray = splitData .filter((item) => item !== '' && item.startsWith('[')) .flatMap((item) => JSON.parse(item)); const sortedItems = consoleUi.omitAndSortDevModeActivity(jsonArray); for (const activity of sortedItems) { const resourceName = ((activity.tags || {}).aws || {}).resourceName; const time = consoleUi.formatConsoleDate(new Date(activity.timestamp)); const tryPrintJSON = (str) => { try { const parsedBody = JSON.parse(str); if (typeof parsedBody === 'string') { throw new Error('Not a JSON object'); } const colors = activity.severityText === 'ERROR' ? jsonError : jsonColors; process.stdout.write(`${colorize(JSON.stringify(parsedBody, null, 2), colors)}\n`); } catch (error) { process.stdout.write(chalk.white(`${str}${str.endsWith('\n') ? '' : '\n'}`)); } }; switch (activity.type) { case 'log': consoleMonitoringCounter.logBatches += 1; process.stdout.write(headerChalk(`\n${time} • ${resourceName} • Log\n`)); tryPrintJSON(activity.body); break; case 'span': { const span = consoleUi.formatConsoleSpan(activity); process.stdout.write( headerChalk(`\n${time} • ${resourceName} • Span • ${span.niceName}\n`) ); if (verbose) { if (activity.input) { process.stdout.write(headerChalk('Input\n')); tryPrintJSON(activity.input); } if (activity.output) { process.stdout.write(headerChalk('Output\n')); tryPrintJSON(activity.output); } } break; } case 'aws-lambda-request': process.stdout.write(headerChalk(`\n${time} • ${resourceName} • Invocation Started\n`)); if (verbose) { tryPrintJSON(activity.body); } break; case 'aws-lambda-response': consoleMonitoringCounter.responses += 1; process.stdout.write(headerChalk(`\n${time} • ${resourceName} • Invocation Ended\n`)); if (verbose) { tryPrintJSON(activity.body); } if (errorTracker[activity.traceId]) { const uiLink = `${frontend}/${ context.org.orgName }/explorer?explorerSubScope=invocations&explorerTraceId=${encodeURIComponent( activity.traceId )}&explorerTraceTime${ errorTracker[activity.traceId] }&globalScope=awsLambda&globalTimeFrame=24h`; process.stdout.write(chalk.white(`View full trace: ${uiLink}\n`)); delete errorTracker[activity.traceId]; } break; case 'event': { consoleMonitoringCounter.events += 1; const { message, payload } = consoleUi.formatConsoleEvent(activity); const isError = /ERROR •/.test(message); const headerWriter = isError ? chalk.hex('#FD5750') : headerChalk; const options = isError ? jsonError : jsonColors; process.stdout.write(headerWriter(`\n${time} • ${resourceName} • ${message}\n`)); process.stdout.write(`${colorize(JSON.stringify(payload, null, 2), options)}\n`); if (isError) { errorTracker[activity.traceId] = activity.timestamp; } break; } default: } } } catch (error) { process.stdout.write(error, '\n'); } }; const connectToWebSocket = async ({ functionName, region, accountId, org, state }) => { const { token } = await apiRequest(`/api/identity/orgs/${org.orgId}/token`); const ws = new WebSocket(`${devModeFeed}?Auth=${token}`, { perMessageDeflate: false, }); ws.on('open', () => { if (state && state === 'firstConnection') { const functionNameQueryParams = functionName .map((name) => `devModeFunctionName=${encodeURIComponent(name)}`) .join('&'); const uiLink = `${frontend}/${org.orgName}/dev-mode?devModeCloudAccountId=${accountId}&${functionNameQueryParams}`; writeText( style.aside( '\n• Use the `--verbose` flag to see inputs and outputs of all requests (e.g. DynamoDB inputs/outputs).', `• Use the Console Dev Mode UI for deeper inspection: ${uiLink}\n`, 'Waiting for activity... Invoke your functions now.' ) ); } else if (state && state === 'resume') { writeText(style.aside('\nResuming for dev mode activity...')); } ws.send( JSON.stringify({ filters: { functionName, region: [region], accountId: [accountId] } }) ); }); ws.on('message', (data) => { streamBuff.put(`${data.toString('utf-8')};;;`); }); return ws; }; const startDevModeFeed = async (context, devModeFeedConnection) => new Promise((resolve) => { const createStillWorkingTimeout = () => setTimeout(async () => { clearInterval(eventPublishTimer); clearInterval(connectionRefreshTimer); devModeFeedConnection.terminate(); writeText(style.aside('Pausing for dev mode activity.\n')); const shouldContinue = await promptWithHistory({ name: 'shouldContinue', message: 'Are you still working?', stepHistory: context.stepHistory, type: 'confirm', }); if (shouldContinue) { await startDevModeFeed(context, 'resume'); } resolve(); }, 1000 * 60 * 60 * 1.5); // Check for activity every 1.5 hours let stillWorkingTimer = createStillWorkingTimeout(); const connectionRefreshTimer = setInterval(async () => { const newConnection = await connectToWebSocket({ functionName: context.consoleDevModeTargetFunctions, accountId: context.awsAccountId, region: context.serverless.service.provider.region, org: context.org, state: false, }); const oldConnection = devModeFeedConnection; oldConnection.terminate(); devModeFeedConnection = newConnection; watchStream(devModeFeedConnection); }, 1000 * 60 * 60); // Refresh every hour const eventPublishTimer = setInterval(async () => { const { userId } = await apiRequest('/api/identity/me'); const body = { source: 'web.dev_mode.activity.v1', event: { orgUid: context.org.orgId, userId, logBatches: consoleMonitoringCounter.logBatches, responses: consoleMonitoringCounter.responses, events: consoleMonitoringCounter.events, source: 'cli:serverless', }, }; await apiRequest('/api/events/publish', { method: 'POST', body, }); consoleMonitoringCounter.logBatches = 0; consoleMonitoringCounter.responses = 0; consoleMonitoringCounter.events = 0; }, 1000 * 60); // Publish every 60 seconds const watchStream = (feed) => { feed.on('message', (data) => { // Ignore connection message const parsedData = JSON.parse(data.toString('utf-8')); if (!parsedData.resetThrottle) { clearTimeout(stillWorkingTimer); stillWorkingTimer = createStillWorkingTimeout(); } }); feed.on('close', () => { // Clean up if we receive a close event clearInterval(eventPublishTimer); clearInterval(connectionRefreshTimer); clearTimeout(stillWorkingTimer); resolve(); }); }; watchStream(devModeFeedConnection); }); module.exports = { async isApplicable(context) { const { isConsoleDevMode, org, consoleDevModeTargetFunctions } = context; if (!isConsoleDevMode) { context.inapplicabilityReasonCode = 'NON_DEV_MODE_CONTEXT'; return false; } if (!org) { context.inapplicabilityReasonCode = 'UNRESOLVED_ORG'; return false; } if (!consoleDevModeTargetFunctions) { context.inapplicabilityReasonCode = 'NO_TARGET_FUNCTIONS'; return false; } return true; }, async run(context) { const devModeProgress = progress.get('dev-mode-progress'); devModeProgress.remove(); log.notice.success('Dev Mode Initialized.'); streamBuff.on('data', handleSocketMessage(context)); const devModeFeedConnection = await connectToWebSocket({ functionName: context.consoleDevModeTargetFunctions, accountId: context.awsAccountId, region: context.serverless.service.provider.region, org: context.org, state: 'firstConnection', }); await startDevModeFeed(context, devModeFeedConnection); }, };