diff --git a/package-lock.json b/package-lock.json index f63530c8..9eefe1fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@thundra/core", - "version": "2.8.1", + "version": "2.8.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -804,9 +804,9 @@ } }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "dev": true }, "async-listener": { @@ -1896,11 +1896,6 @@ "integrity": "sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA==", "dev": true }, - "bindings": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", - "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" - }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -2959,23 +2954,6 @@ "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "dev": true }, - "deasync": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.15.tgz", - "integrity": "sha512-pxMaCYu8cQIbGkA4Y1R0PLSooPIpH1WgFBLeJ+zLxQgHfkZG86ViJSmZmONSjZJ/R3NjwkMcIWZAzpLB2G9/CA==", - "requires": { - "bindings": "~1.2.1", - "node-addon-api": "^1.6.0" - } - }, - "deasync-promise": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deasync-promise/-/deasync-promise-1.0.1.tgz", - "integrity": "sha1-KyfeR4Fnr07zS6mYecUuwM7dYcI=", - "requires": { - "deasync": "^0.1.7" - } - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7165,6 +7143,18 @@ "whatwg-url": "^6.4.1", "ws": "^4.0.0", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0" + } + } } }, "jsesc": { @@ -8090,11 +8080,6 @@ } } }, - "node-addon-api": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz", - "integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==" - }, "node-fetch": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", @@ -10803,14 +10788,6 @@ "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, - "system-sleep": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/system-sleep/-/system-sleep-1.3.6.tgz", - "integrity": "sha512-tBQqFUdmuFQhiXtptJR0Nu+fOL0lGFRML2BD0G02bCCCHAZ09Qmu0M/nPzjJL/qx23zQ7arFMo2/LCvfSsgJZA==", - "requires": { - "deasync-promise": "1.0.1" - } - }, "table": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", @@ -11699,14 +11676,9 @@ } }, "ws": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", - "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0" - } + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", + "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==" }, "xdg-basedir": { "version": "3.0.0", @@ -11732,7 +11704,7 @@ }, "xmlbuilder": { "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", "dev": true }, diff --git a/package.json b/package.json index 0e680d7e..d59291ef 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "require-in-the-middle": "^3.1.0", "semver": "^5.5.0", "shimmer": "^1.2.0", - "system-sleep": "^1.3.6", - "uuid": "^3.2.1" + "uuid": "^3.2.1", + "ws": "^7.2.1" } } diff --git a/rollup.config.js b/rollup.config.js index 3c5d7e09..efe045e1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -39,6 +39,28 @@ module.exports = [ }) ] }, + { + input: './src/debugBridge.ts', + output: { + file: 'dist/debugBridge.js', + format: 'cjs', + }, + plugins: [ + typescript(), + terser({ + warnings: 'verbose', + compress: { + warnings: 'verbose', + }, + mangle: { + keep_fnames: true, + }, + output: { + beautify: false, + }, + }), + ], + }, { input: './src/handler.ts', output: { diff --git a/src/Constants.ts b/src/Constants.ts index 18b18518..c2a1cb08 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -96,10 +96,12 @@ export const envVariableKeys = { THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE: 'thundra_agent_lambda_debugger_enable', THUNDRA_AGENT_LAMBDA_DEBUGGER_PORT: 'thundra_agent_lambda_debugger_port', + THUNDRA_AGENT_LAMBDA_DEBUGGER_LOGS_ENABLE: 'thundra_agent_lambda_debugger_logs_enable', THUNDRA_AGENT_LAMBDA_DEBUGGER_WAIT_MAX: 'thundra_agent_lambda_debugger_wait_max', THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_PORT: 'thundra_agent_lambda_debugger_broker_port', THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_HOST: 'thundra_agent_lambda_debugger_broker_host', - + THUNDRA_AGENT_LAMBDA_DEBUGGER_SESSION_NAME: 'thundra_agent_lambda_debugger_session_name', + THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN: 'thundra_agent_lambda_debugger_auth_token', }; export function getTimeoutMargin(region: string) { @@ -719,4 +721,14 @@ export const StdErrorLogContext = 'STDERR'; export const DefaultMongoCommandSizeLimit = 128 * 1024; export const DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_PORT = 1111; -export const DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_HOST = 'debug.thundra.io'; +export const DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_HOST = 'debug.thundra.io'; +export const DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_PORT = 444; +export const DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_SESSION_NAME = 'default'; +export const DEBUG_BRIDGE_FILE_NAME = 'debugBridge.js'; +export const BROKER_WS_PROTOCOL = 'wss://'; +export const BROKER_WS_HTTP_ERROR_PATTERN = /:\s*\D*(\d+)/; +export const BROKER_WS_HTTP_ERR_CODE_TO_MSG: {[key: number]: string} = { + 429: `Reached the concurrent session limit, couldn't start Thundra debugger.`, + 401: `Authentication is failed, check your Thundra debugger authentication token.`, + 409: `Another session with the same session name exists, connection closed.`, +}; diff --git a/src/ThundraWrapper.ts b/src/ThundraWrapper.ts index e8d81145..3b2c7e48 100644 --- a/src/ThundraWrapper.ts +++ b/src/ThundraWrapper.ts @@ -26,10 +26,16 @@ import InvocationSupport from './plugins/support/InvocationSupport'; import { envVariableKeys, DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_PORT, - DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_HOST, + DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_HOST, + DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_PORT, + DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_SESSION_NAME, + DEBUG_BRIDGE_FILE_NAME, BROKER_WS_HTTP_ERROR_PATTERN, + BROKER_WS_HTTP_ERR_CODE_TO_MSG, BROKER_WS_PROTOCOL, } from './Constants'; import Utils from './plugins/utils/Utils'; -import { readFileSync, existsSync } from 'fs'; +import { readFileSync } from 'fs'; + +const path = require('path'); class ThundraWrapper { @@ -48,15 +54,20 @@ class ThundraWrapper { private resolve: any; private reject: any; private inspector: any; - private spawn: any; + private fork: any; private debuggerPort: number; private debuggerMaxWaitTime: number; + private monitoringDisabled: boolean; private brokerHost: string; + private sessionName: string; + private authToken: string; + private sessionTimeout: number; private brokerPort: number; private debuggerProxy: any; + private debuggerLogsEnabled: boolean; constructor(self: any, event: any, context: any, callback: any, - originalFunction: any, plugins: any, pluginContext: PluginContext) { + originalFunction: any, plugins: any, pluginContext: PluginContext, monitoringDisabled: boolean) { this.originalThis = self; this.originalEvent = event; this.originalContext = context; @@ -68,6 +79,7 @@ class ThundraWrapper { this.pluginContext.maxMemory = parseInt(context.memoryLimitInMB, 10); this.reported = false; this.reporter = new Reporter(pluginContext.config); + this.monitoringDisabled = monitoringDisabled; this.wrappedContext = { ...context, done: (error: any, result: any) => { @@ -100,7 +112,7 @@ class ThundraWrapper { this.timeout = this.setupTimeoutHandler(this); InvocationSupport.setFunctionName(this.originalContext.functionName); - if (Utils.getConfiguration(envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE) === 'true') { + if (this.shouldInitDebugger()) { this.initDebugger(); } } @@ -111,6 +123,17 @@ class ThundraWrapper { }); } + shouldInitDebugger(): boolean { + const authToken = Utils.getConfiguration(envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN); + const debuggerEnable = Utils.getConfiguration(envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE); + + if (debuggerEnable === 'false' || !authToken) { + return false; + } + + return true; + } + invokeCallback(error: any, result: any): void { if (typeof this.originalCallback === 'function') { this.originalCallback(error, result); @@ -118,24 +141,18 @@ class ThundraWrapper { } onFinish(error: any, result: any): void { + this.finishDebuggerProxyIfAvailable(); if (error && this.reject) { this.reject(error); } else if (this.resolve) { this.resolve(result); } - this.finishDebuggerProxyIfAvailable(); } initDebugger(): void { try { - if (!existsSync('/opt/socat')) { - throw new Error( - '"Socat" is not exist under "/opt/socat". \ - Please be sure that "socat" layer is added or it is available under "/opt/socat"'); - } - this.inspector = require('inspector'); - this.spawn = require('child_process').spawn; + this.fork = require('child_process').fork; const debuggerPort = Utils.getNumericConfiguration( @@ -144,15 +161,27 @@ class ThundraWrapper { const brokerHost = Utils.getConfiguration( envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_HOST, - DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_HOST); + DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_HOST); const brokerPort = Utils.getNumericConfiguration( envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_PORT, - -1); + DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_BROKER_PORT); + const authToken = + Utils.getConfiguration( + envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN, + ''); + const sessionName = + Utils.getConfiguration( + envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_SESSION_NAME, + DEFAULT_THUNDRA_AGENT_LAMBDA_DEBUGGER_SESSION_NAME); const debuggerMaxWaitTime = Utils.getNumericConfiguration( envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_WAIT_MAX, 60 * 1000); + const debuggerLogsEnabled = + Utils.getConfiguration( + envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_LOGS_ENABLE, + false) === 'true'; if (brokerPort === -1) { throw new Error( @@ -164,8 +193,12 @@ class ThundraWrapper { this.debuggerMaxWaitTime = debuggerMaxWaitTime; this.brokerPort = brokerPort; this.brokerHost = brokerHost; + this.sessionName = sessionName; + this.sessionTimeout = Date.now() + this.originalContext.getRemainingTimeInMillis(); + this.authToken = authToken; + this.debuggerLogsEnabled = debuggerLogsEnabled; } catch (e) { - this.spawn = null; + this.fork = null; this.inspector = null; } } @@ -183,19 +216,21 @@ class ThundraWrapper { } } - waitForDebugger(): void { - const sleep = require('system-sleep'); + async waitForDebugger() { let prevRchar = 0; let prevWchar = 0; let initCompleted = false; const logger: ThundraLogger = ThundraLogger.getInstance(); logger.info('Waiting for debugger to handshake ...'); + + const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + const startTime = Date.now(); while ((Date.now() - startTime) < this.debuggerMaxWaitTime) { try { const debuggerIoMetrics = this.getDebuggerProxyIOMetrics(); if (!debuggerIoMetrics) { - sleep(1000); + await sleep(1000); break; } if (prevRchar !== 0 && prevWchar !== 0 && @@ -209,7 +244,7 @@ class ThundraWrapper { logger.error(e); break; } - sleep(1000); + await sleep(1000); } if (initCompleted) { logger.info('Completed debugger handshake'); @@ -218,22 +253,63 @@ class ThundraWrapper { } } - startDebuggerProxyIfAvailable(): void { + async startDebuggerProxyIfAvailable() { if (this.debuggerProxy) { this.finishDebuggerProxyIfAvailable(); } - if (this.spawn && this.inspector) { + if (this.fork && this.inspector) { try { - this.debuggerProxy = - this.spawn( - '/opt/socat', - [ - 'TCP:' + this.brokerHost + ':' + this.brokerPort, - 'TCP:localhost:' + this.debuggerPort + ',forever', - ], - {detached: true}); - this.inspector.open(this.debuggerPort, 'localhost', true); - this.waitForDebugger(); + this.debuggerProxy = this.fork( + path.join(__dirname, DEBUG_BRIDGE_FILE_NAME), + [], + { + detached: true, + env: { + BROKER_HOST: this.brokerHost, + BROKER_PORT: this.brokerPort, + SESSION_NAME: this.sessionName, + SESSION_TIMEOUT: this.sessionTimeout, + AUTH_TOKEN: this.authToken, + DEBUGGER_PORT: this.debuggerPort, + LOGS_ENABLED: this.debuggerLogsEnabled, + BROKER_WS_PROTOCOL, + }, + }, + ); + this.inspector.open(this.debuggerPort, 'localhost', false); + + const waitForBrokerConnection = () => new Promise((resolve) => { + this.debuggerProxy.once('message', (mes: any) => { + if (mes === 'brokerConnect') { + return resolve(false); + } + + let errMessage: string; + if (typeof mes === 'string') { + const match = mes.match(BROKER_WS_HTTP_ERROR_PATTERN); + + if (match) { + const errCode = Number(match[1]); + errMessage = BROKER_WS_HTTP_ERR_CODE_TO_MSG[errCode]; + } + } + + // If errMessage is undefined replace it with the raw incoming message + errMessage = errMessage || mes; + ThundraLogger.getInstance().error('Thundra Debugger: ' + errMessage); + + return resolve(true); + }); + }); + + const brokerHasErr = await waitForBrokerConnection(); + + if (brokerHasErr) { + this.finishDebuggerProxyIfAvailable(); + return; + } + + await this.waitForDebugger(); } catch (e) { this.debuggerProxy = null; ThundraLogger.getInstance().error(e); @@ -245,13 +321,16 @@ class ThundraWrapper { try { if (this.inspector) { this.inspector.close(); + this.inspector = null; } } catch (e) { ThundraLogger.getInstance().error(e); } if (this.debuggerProxy) { try { - this.debuggerProxy.kill('SIGKILL'); + if (!this.debuggerProxy.killed) { + this.debuggerProxy.kill(); + } } catch (e) { ThundraLogger.getInstance().error(e); } finally { @@ -260,10 +339,10 @@ class ThundraWrapper { } } - invoke() { - // Refresh config to check if config updated + async invoke() { this.config.refreshConfig(); - this.startDebuggerProxyIfAvailable(); + + await this.startDebuggerProxyIfAvailable(); const beforeInvocationData = { originalContext: this.originalContext, @@ -330,6 +409,10 @@ class ThundraWrapper { } async executeAfteInvocationAndReport(afterInvocationData: any) { + if (this.monitoringDisabled) { + return; + } + afterInvocationData.error ? InvocationSupport.setErrorenous(true) : InvocationSupport.setErrorenous(false); await this.executeHook('after-invocation', afterInvocationData, true); @@ -426,6 +509,18 @@ class ThundraWrapper { const endTime = Math.min(configEndTime, maxEndTime); return setTimeout(() => { + if (this.debuggerProxy) { + // Debugger proxy exists, let it know about the timeout + try { + if (!this.debuggerProxy.killed) { + this.debuggerProxy.kill('SIGHUP'); + } + } catch (e) { + ThundraLogger.getInstance().error(e); + } finally { + this.debuggerProxy = null; + } + } wrapperInstance.report(new TimeoutError('Lambda is timed out.'), null, null); wrapperInstance.reported = false; }, endTime); diff --git a/src/debugBridge.ts b/src/debugBridge.ts new file mode 100644 index 00000000..f3d2ccb3 --- /dev/null +++ b/src/debugBridge.ts @@ -0,0 +1,100 @@ +const net = require('net'); +const WebSocket = require('ws'); +const { DEBUGGER_PORT, BROKER_HOST, BROKER_PORT, + LOGS_ENABLED, AUTH_TOKEN, SESSION_NAME, SESSION_TIMEOUT, BROKER_WS_PROTOCOL } = process.env; + +const CLOSING_CODES: {[key: string]: number} = { + NORMAL: 1000, + TIMEOUT: 4000, +}; + +const log = (...params: any[]) => { + if (LOGS_ENABLED === 'true') { + console.log(...params); + } +}; + +const debuggerSocket = new net.Socket(); +const brokerSocket = new WebSocket( + `${BROKER_WS_PROTOCOL}${BROKER_HOST}:${BROKER_PORT}`, + { + headers: { + 'x-thundra-auth-token': AUTH_TOKEN, + 'x-thundra-session-name': SESSION_NAME, + 'x-thundra-protocol-version': '1.0', + 'x-thundra-session-timeout': SESSION_TIMEOUT, + }, + }, +); + +// Setup debugger socket +debuggerSocket.on('data', (data: Buffer) => { + brokerSocket.send(data); +}); +debuggerSocket.on('end', () => { + log('debuggerSocket: disconnected from the main lambda process'); + if (brokerSocket.readyState === WebSocket.OPEN) { + brokerSocket.close(CLOSING_CODES.NORMAL, 'Normal'); + } +}); +debuggerSocket.on('error', (err: Error) => { + log('debuggerSocket:', err.message); +}); + +// Setup broker socket +let firstMessage = true; +let brokerHSSuccess = false; +const sendToDebugger = (data: Buffer) => { + if (debuggerSocket.writable) { + debuggerSocket.write(data); + } else { + setTimeout(() => sendToDebugger(data), 0); + } +}; + +brokerSocket.on('message', (data: Buffer) => { + if (firstMessage) { + firstMessage = false; + process.send('brokerConnect'); + debuggerSocket.connect({ port: DEBUGGER_PORT }, () => { + log('debuggerSocket: connection established with main lambda process'); + }); + } + + sendToDebugger(data); +}); +brokerSocket.on('open', () => { + brokerHSSuccess = true; + log('brokerSocket: connection established with the Thundra broker'); +}); +brokerSocket.on('close', (code: Number, reason: string) => { + log(`brokerSocket: disconnected from the the Thundra broker, code: ${code}, reason: ${reason}`); + if (!debuggerSocket.destroyed) { + debuggerSocket.end(); + } +}); +brokerSocket.on('error', (err: Error) => { + if (!brokerHSSuccess) { + // Error occured before handshake, main process should know it + process.send(err.message); + } + log('brokerSocket:', err.message); +}); + +process.on('SIGTERM', () => { + if (brokerSocket.readyState === WebSocket.OPEN) { + brokerSocket.close(CLOSING_CODES.NORMAL, 'Normal'); + } + if (!debuggerSocket.destroyed) { + debuggerSocket.end(); + } +}); + +process.on('SIGHUP', () => { + if (brokerSocket.readyState === WebSocket.OPEN) { + brokerSocket.close(CLOSING_CODES.TIMEOUT, 'SessionTimeout'); + } + if (!debuggerSocket.destroyed) { + debuggerSocket.end(); + } +}); diff --git a/src/index.ts b/src/index.ts index 84da6f66..0ecc55aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,43 +43,51 @@ module.exports = (options?: any) => { const config = new ThundraConfig(options); const plugins: any[] = []; - if (!(config.apiKey) || config.disableThundra) { + if (config.disableThundra) { return (originalFunc: any) => originalFunc; } - if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_TRACE) === 'true') && config.traceConfig.enabled) { - const tracePlugin = TracePlugin(config.traceConfig); - plugins.push(tracePlugin); - - tracer = tracePlugin.tracer; - config.metricConfig.tracer = tracer; - config.logConfig.tracer = tracer; - config.xrayConfig.tracer = tracer; - InvocationTraceSupport.tracer = tracer; + if (!(config.apiKey)) { + console.warn(`Thundra API Key is not given, monitoring is disabled.`); } - if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_METRIC, 'true') === 'true') && config.metricConfig.enabled) { - const metricPlugin = MetricPlugin(config.metricConfig); - plugins.push(metricPlugin); - } + const monitoringDisabled: boolean = !(config.apiKey); + + if (!monitoringDisabled) { + if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_TRACE) === 'true') && config.traceConfig.enabled) { + const tracePlugin = TracePlugin(config.traceConfig); + plugins.push(tracePlugin); - if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_LOG, 'true') === 'true') && config.logConfig.enabled) { - if (!Log.getInstance()) { - const logPlugin = new Log(config.logConfig); - Logger.getLogManager().addListener(logPlugin); + tracer = tracePlugin.tracer; + config.metricConfig.tracer = tracer; + config.logConfig.tracer = tracer; + config.xrayConfig.tracer = tracer; + InvocationTraceSupport.tracer = tracer; } - const logInstance = Log.getInstance(); - logInstance.enable(); - plugins.push(logInstance); - } - if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_XRAY) === 'true') && config.xrayConfig.enabled) { - const aws = AwsXRayPlugin(config.metricConfig); - plugins.push(aws); - } + if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_METRIC, 'true') === 'true') && config.metricConfig.enabled) { + const metricPlugin = MetricPlugin(config.metricConfig); + plugins.push(metricPlugin); + } - const invocationPlugin = InvocationPlugin(config.invocationConfig); - plugins.push(invocationPlugin); + if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_LOG, 'true') === 'true') && config.logConfig.enabled) { + if (!Log.getInstance()) { + const logPlugin = new Log(config.logConfig); + Logger.getLogManager().addListener(logPlugin); + } + const logInstance = Log.getInstance(); + logInstance.enable(); + plugins.push(logInstance); + } + + if (!(Utils.getConfiguration(envVariableKeys.THUNDRA_DISABLE_XRAY) === 'true') && config.xrayConfig.enabled) { + const aws = AwsXRayPlugin(config.metricConfig); + plugins.push(aws); + } + + const invocationPlugin = InvocationPlugin(config.invocationConfig); + plugins.push(invocationPlugin); + } if (config.trustAllCert) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; @@ -111,7 +119,7 @@ module.exports = (options?: any) => { return originalFunc; } - const thundraWrappedHandler: any = (originalEvent: any, originalContext: any, originalCallback: any) => { + const thundraWrappedHandler: any = async (originalEvent: any, originalContext: any, originalCallback: any) => { // Creating applicationId here, since we need the information in context const applicationId = Utils.getApplicationId(originalContext, pluginContext); pluginContext.applicationId = applicationId; @@ -129,8 +137,9 @@ module.exports = (options?: any) => { originalFunc, plugins, pluginContext, + monitoringDisabled, ); - return thundraWrapper.invoke(); + return await thundraWrapper.invoke(); }; // Set thundraWrapped to true, to not double wrap the user handler thundraWrappedHandler.thundraWrapped = true; diff --git a/test/index.test.js b/test/index.test.js index 82df35e6..46b36a09 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,6 +1,7 @@ const Thundra = require('../dist/index'); import Utils from '../dist/plugins/utils/Utils.js'; import { createMockContext } from './mocks/mocks'; +import ThundraWrapper from '../dist/ThundraWrapper'; beforeAll(() => { Utils.readProcIoPromise = jest.fn(() => { @@ -14,6 +15,8 @@ beforeAll(() => { return resolve({ threadCount: 10 }); }); }); + + ThundraWrapper.prototype.executeAfteInvocationAndReport = jest.fn(); }); describe('thundra library', () => { @@ -21,7 +24,7 @@ describe('thundra library', () => { const originalEvent = { key: 'value' }; const originalContext = createMockContext(); const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -30,7 +33,7 @@ describe('thundra library', () => { thundraWrapper = Thundra(); wrappedFunction = thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should invoke the function', () => { @@ -43,41 +46,20 @@ describe('thundra library', () => { }); describe('thundra disabled', () => { - describe('by no apiKey', () => { - const originalEvent = { key: 'value' }; - const originalContext = {}; - const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - let ThundraWrapper; - let wrappedFunction; - - beforeAll(() => { - delete process.env.thundra_apiKey; - - ThundraWrapper = Thundra(); - wrappedFunction = ThundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); - }); - - it('should not wrap', () => { - expect(wrappedFunction).toBe(originalFunction); - }); - }); - describe('by parameter', () => { const originalEvent = { key: 'value' }; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - let ThundraWrapper; + const originalFunction = jest.fn((event, context, callback) => callback()); + let thundraWrapper; let wrappedFunction; beforeAll(() => { delete process.env.thundra_agent_lambda_disable; - ThundraWrapper = Thundra({ apiKey: 'apiKey', disableThundra: true, plugins: [] }); - wrappedFunction = ThundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + thundraWrapper = Thundra({ apiKey: 'apiKey', disableThundra: true, plugins: [] }); + wrappedFunction = thundraWrapper(originalFunction); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should not wrap', () => { @@ -89,7 +71,7 @@ describe('thundra library', () => { const originalEvent = { key: 'value' }; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -97,7 +79,7 @@ describe('thundra library', () => { process.env.thundra_agent_lambda_disable = 'true'; thundraWrapper = Thundra({ apiKey: 'apiKey' }); wrappedFunction = thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should not wrap', () => { @@ -111,7 +93,7 @@ describe('thundra library', () => { const originalEvent = {key: 'value'}; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -123,7 +105,7 @@ describe('thundra library', () => { thundraWrapper = Thundra({apiKey: 'apiKey', disableTrace: true, disableMetric: true}); wrappedFunction = thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should invoke the function', () => { @@ -138,7 +120,7 @@ describe('thundra library', () => { const originalEvent = {key: 'value'}; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -150,7 +132,7 @@ describe('thundra library', () => { thundraWrapper = Thundra({apiKey: 'apiKey'}); wrappedFunction = thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should invoke the function', () => { @@ -165,7 +147,7 @@ describe('thundra library', () => { const originalEvent = {key: 'value'}; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -177,7 +159,7 @@ describe('thundra library', () => { thundraWrapper = Thundra({apiKey: 'apiKey'}); wrappedFunction = thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should invoke the function', () => { @@ -193,7 +175,7 @@ describe('thundra library', () => { const originalEvent = {}; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -204,7 +186,7 @@ describe('thundra library', () => { console.log = jest.fn(); jest.useFakeTimers(); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); jest.runAllTimers(); }); @@ -217,7 +199,7 @@ describe('thundra library', () => { const originalEvent = {}; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -226,10 +208,7 @@ describe('thundra library', () => { thundraWrapper = Thundra({ apiKey: 'apiKey' }); wrappedFunction = thundraWrapper(originalFunction); - console['log'] = jest.fn(); - jest.useFakeTimers(); - wrappedFunction(originalEvent, originalContext, originalCallback); - jest.runAllTimers(); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should invoke the function', () => { @@ -243,7 +222,7 @@ describe('thundra library', () => { const originalEvent = { key: 'value' }; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -253,7 +232,7 @@ describe('thundra library', () => { thundraWrapper = new Thundra({ apiKey: 'apiKey', trustAllCert: true }); wrappedFunction = new thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should set environment variable', () => { @@ -265,7 +244,7 @@ describe('thundra library', () => { const originalEvent = { key: 'value' }; const originalContext = {}; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((event, context, callback) => callback()); let thundraWrapper; let wrappedFunction; @@ -276,7 +255,7 @@ describe('thundra library', () => { thundraWrapper = new Thundra({ apiKey: 'apiKey', trustAllCert: true }); wrappedFunction = new thundraWrapper(originalFunction); - wrappedFunction(originalEvent, originalContext, originalCallback); + return wrappedFunction(originalEvent, originalContext, originalCallback); }); it('should set environment variable', () => { diff --git a/test/integration/http.integration.test.js b/test/integration/http.integration.test.js index e35a52f3..d93dd188 100644 --- a/test/integration/http.integration.test.js +++ b/test/integration/http.integration.test.js @@ -4,7 +4,7 @@ import Http from './utils/http.integration.utils'; import InvocationSupport from '../../dist/plugins/support/InvocationSupport'; describe('HTTP integration', () => { - test('should instrument HTTP GET calls ', () => { + test('should instrument HTTP GET calls ', async () => { const tracer = new ThundraTracer(); const integration = new HttpIntegration({ httpPathDepth: 2, @@ -14,29 +14,29 @@ describe('HTTP integration', () => { InvocationSupport.setFunctionName('functionName'); - return Http.get(sdk).then(() => { - const span = tracer.getRecorder().spanList[0]; - - expect(span.operationName).toBe('jsonplaceholder.typicode.com/users/1'); - expect(span.className).toBe('HTTP'); - expect(span.domainName).toBe('API'); - - expect(span.tags['operation.type']).toBe('GET'); - expect(span.tags['http.method']).toBe('GET'); - expect(span.tags['http.host']).toBe('jsonplaceholder.typicode.com'); - expect(span.tags['http.path']).toBe('/users/1'); - expect(span.tags['http.url']).toBe('jsonplaceholder.typicode.com/users/1?q=123'); - expect(span.tags['http.query_params']).toBe('q=123'); - expect(span.tags['http.status_code']).toBe(200); - expect(span.tags['topology.vertex']).toEqual(true); - expect(span.tags['trigger.domainName']).toEqual('API'); - expect(span.tags['trigger.className']).toEqual('AWS-Lambda'); - expect(span.tags['trigger.operationNames']).toEqual(['functionName']); - expect(span.tags['http.body']).not.toBeTruthy(); - }); + await Http.get(sdk); + + const span = tracer.getRecorder().spanList[0]; + + expect(span.operationName).toBe('jsonplaceholder.typicode.com/users/1'); + expect(span.className).toBe('HTTP'); + expect(span.domainName).toBe('API'); + + expect(span.tags['operation.type']).toBe('GET'); + expect(span.tags['http.method']).toBe('GET'); + expect(span.tags['http.host']).toBe('jsonplaceholder.typicode.com'); + expect(span.tags['http.path']).toBe('/users/1'); + expect(span.tags['http.url']).toBe('jsonplaceholder.typicode.com/users/1?q=123'); + expect(span.tags['http.query_params']).toBe('q=123'); + expect(span.tags['http.status_code']).toBe(200); + expect(span.tags['topology.vertex']).toEqual(true); + expect(span.tags['trigger.domainName']).toEqual('API'); + expect(span.tags['trigger.className']).toEqual('AWS-Lambda'); + expect(span.tags['trigger.operationNames']).toEqual(['functionName']); + expect(span.tags['http.body']).not.toBeTruthy(); }); - test('should set 4XX 5XX errors on HTTP calls', () => { + test('should set 4XX 5XX errors on HTTP calls', async () => { const tracer = new ThundraTracer(); const integration = new HttpIntegration({ httpPathDepth: 2, @@ -46,64 +46,63 @@ describe('HTTP integration', () => { InvocationSupport.setFunctionName('functionName'); - return Http.getError(sdk).then(() => { - const span = tracer.getRecorder().spanList[0]; - expect(span.operationName).toBe('httpstat.us/404'); - expect(span.className).toBe('HTTP'); - expect(span.domainName).toBe('API'); - - expect(span.tags['operation.type']).toBe('GET'); - expect(span.tags['http.method']).toBe('GET'); - expect(span.tags['http.host']).toBe('httpstat.us'); - expect(span.tags['http.path']).toBe('/404'); - expect(span.tags['http.url']).toBe('httpstat.us/404'); - expect(span.tags['http.status_code']).toBe(404); - expect(span.tags['error']).toBe(true); - expect(span.tags['error.kind']).toBe('HttpError'); - expect(span.tags['error.message']).toBe('Not Found'); - expect(span.tags['topology.vertex']).toEqual(true); - expect(span.tags['trigger.domainName']).toEqual('API'); - expect(span.tags['trigger.className']).toEqual('AWS-Lambda'); - expect(span.tags['trigger.operationNames']).toEqual(['functionName']); - expect(span.tags['http.body']).not.toBeTruthy(); - }); + await Http.getError(sdk); + + const span = tracer.getRecorder().spanList[0]; + expect(span.operationName).toBe('httpstat.us/404'); + expect(span.className).toBe('HTTP'); + expect(span.domainName).toBe('API'); + + expect(span.tags['operation.type']).toBe('GET'); + expect(span.tags['http.method']).toBe('GET'); + expect(span.tags['http.host']).toBe('httpstat.us'); + expect(span.tags['http.path']).toBe('/404'); + expect(span.tags['http.url']).toBe('httpstat.us/404'); + expect(span.tags['http.status_code']).toBe(404); + expect(span.tags['error']).toBe(true); + expect(span.tags['error.kind']).toBe('HttpError'); + expect(span.tags['error.message']).toBe('Not Found'); + expect(span.tags['topology.vertex']).toEqual(true); + expect(span.tags['trigger.domainName']).toEqual('API'); + expect(span.tags['trigger.className']).toEqual('AWS-Lambda'); + expect(span.tags['trigger.operationNames']).toEqual(['functionName']); + expect(span.tags['http.body']).not.toBeTruthy(); }); - test('should disable 4XX 5XX errors on HTTP calls', () => { + test('should disable 4XX 5XX errors on HTTP calls', async () => { const tracer = new ThundraTracer(); const integration = new HttpIntegration({ httpPathDepth: 2, - disableHttp4xxError:true, + disableHttp4xxError: true, tracer, }); const sdk = require('http'); InvocationSupport.setFunctionName('functionName'); - return Http.getError(sdk).then(() => { - const span = tracer.getRecorder().spanList[0]; - expect(span.operationName).toBe('httpstat.us/404'); - expect(span.className).toBe('HTTP'); - expect(span.domainName).toBe('API'); - - expect(span.tags['operation.type']).toBe('GET'); - expect(span.tags['http.method']).toBe('GET'); - expect(span.tags['http.host']).toBe('httpstat.us'); - expect(span.tags['http.path']).toBe('/404'); - expect(span.tags['http.url']).toBe('httpstat.us/404'); - expect(span.tags['http.status_code']).toBe(404); - expect(span.tags['error']).toBe(undefined); - expect(span.tags['error.kind']).toBe(undefined); - expect(span.tags['error.message']).toBe(undefined); - expect(span.tags['topology.vertex']).toEqual(true); - expect(span.tags['trigger.domainName']).toEqual('API'); - expect(span.tags['trigger.className']).toEqual('AWS-Lambda'); - expect(span.tags['trigger.operationNames']).toEqual(['functionName']); - expect(span.tags['http.body']).not.toBeTruthy(); - }); + await Http.getError(sdk); + const span = tracer.getRecorder().spanList[0]; + expect(span.operationName).toBe('httpstat.us/404'); + expect(span.className).toBe('HTTP'); + expect(span.domainName).toBe('API'); + + expect(span.tags['operation.type']).toBe('GET'); + expect(span.tags['http.method']).toBe('GET'); + expect(span.tags['http.host']).toBe('httpstat.us'); + expect(span.tags['http.path']).toBe('/404'); + expect(span.tags['http.url']).toBe('httpstat.us/404'); + expect(span.tags['http.status_code']).toBe(404); + expect(span.tags['error']).toBe(undefined); + expect(span.tags['error.kind']).toBe(undefined); + expect(span.tags['error.message']).toBe(undefined); + expect(span.tags['topology.vertex']).toEqual(true); + expect(span.tags['trigger.domainName']).toEqual('API'); + expect(span.tags['trigger.className']).toEqual('AWS-Lambda'); + expect(span.tags['trigger.operationNames']).toEqual(['functionName']); + expect(span.tags['http.body']).not.toBeTruthy(); }); - test('should instrument HTTPS POST calls', () => { + test('should instrument HTTPS POST calls', async () => { const tracer = new ThundraTracer(); const integration = new HttpIntegration({ httpPathDepth: 0, @@ -113,19 +112,18 @@ describe('HTTP integration', () => { InvocationSupport.setFunctionName('functionName'); - return Http.post(sdk).then(() => { - const span = tracer.getRecorder().spanList[0]; + await Http.post(sdk) + const span = tracer.getRecorder().spanList[0]; - expect(span.operationName).toBe('flaviocopes.com'); - expect(span.className).toBe('HTTP'); - expect(span.domainName).toBe('API'); + expect(span.operationName).toBe('flaviocopes.com'); + expect(span.className).toBe('HTTP'); + expect(span.domainName).toBe('API'); - expect(span.tags['http.method']).toBe('POST'); - expect(span.tags['http.body']).toBe('{"todo":"Buy the milk"}'); - }); + expect(span.tags['http.method']).toBe('POST'); + expect(span.tags['http.body']).toBe('{"todo":"Buy the milk"}'); }); - test('should mask body in post', () => { + test('should mask body in post', async () => { const tracer = new ThundraTracer(); const integration = new HttpIntegration({ maskHttpBody: true, @@ -135,19 +133,19 @@ describe('HTTP integration', () => { InvocationSupport.setFunctionName('functionName'); - return Http.post(sdk).then(() => { - const span = tracer.getRecorder().spanList[0]; - - expect(span.operationName).toBe('flaviocopes.com/todos'); - expect(span.className).toBe('HTTP'); - expect(span.domainName).toBe('API'); + await Http.post(sdk); + + const span = tracer.getRecorder().spanList[0]; - expect(span.tags['http.method']).toBe('POST'); - expect(span.tags['http.body']).not.toBeTruthy(); - }); + expect(span.operationName).toBe('flaviocopes.com/todos'); + expect(span.className).toBe('HTTP'); + expect(span.domainName).toBe('API'); + + expect(span.tags['http.method']).toBe('POST'); + expect(span.tags['http.body']).not.toBeTruthy(); }); - test('should instrument api gateway calls ', () => { + test('should instrument api gateway calls ', () => { const apiGatewayEndpoint = 'hivcx7cj2j.execute-api.us-west-2.amazonaws.com/dev'; const okEndpoint = 'google.com'; const awsEndPoint = 'dynamodb.us-west-2.amazonaws.com'; diff --git a/test/security.test.js b/test/security.test.js index 03b1cf7e..9f601c7c 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -326,7 +326,6 @@ describe('whitelist config', () => { return wrappedFunc(originalEvent, originalContext).then(() => { const span = recorder.spanList[1]; - console.log(span.tags); checkIfWhitelisted(span); }); }); diff --git a/test/thundra-wrapper.test.js b/test/thundra-wrapper.test.js index 23bc0679..37cfca5b 100644 --- a/test/thundra-wrapper.test.js +++ b/test/thundra-wrapper.test.js @@ -1,64 +1,77 @@ import ThundraWrapper from '../dist/ThundraWrapper'; -import {createMockContext, createMockReporterInstance, createMockPlugin, createMockPluginContext, createMockPromise} from './mocks/mocks'; +import { createMockContext, createMockReporterInstance, createMockPlugin, createMockPluginContext, createMockPromise } from './mocks/mocks'; import HttpError from '../dist/plugins/error/HttpError'; import TimeoutError from '../dist/plugins/error/TimeoutError'; +import { envVariableKeys } from '../dist/Constants'; const pluginContext = createMockPluginContext(); -describe('ThundraWrapper', () => { +jest.useFakeTimers(); +describe('ThundraWrapper', () => { + process.env.thundra_agent_lambda_report_cloudwatch_enable = 'false'; const originalThis = this; - const originalEvent = {key1: 'value2', key2: 'value2'}; + const originalEvent = { key1: 'value2', key2: 'value2' }; const originalContext = createMockContext(); const plugins = [createMockPlugin()]; const apiKey = '12345'; - describe('report', async () => { - jest.useFakeTimers(); - process.env.thundra_agent_lambda_report_cloudwatch_enable = 'false'; + describe('report', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => callback()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reporter = createMockReporterInstance(); - await thundraWrapper.report(null, {result: 'result'}, thundraWrapper.originalCallback); - it('should send reports', () => { - expect(thundraWrapper.reporter.sendReports.mock.call.length).toBe(1); - expect(thundraWrapper.reported).toBeTruthy(); + + beforeAll((done) => { + thundraWrapper.report(null, { result: 'result' }, thundraWrapper.originalCallback).then(done); }); - it('should call callback', () => { - expect(originalCallback.mock.call.length).toBe(1); + + test('should send reports', () => { + expect(thundraWrapper.reporter.sendReports).toHaveBeenCalledTimes(1); + expect(thundraWrapper.reported).toBeTruthy(); }); - it('should call clearTimeout', () => { - expect(clearTimeout).toHaveBeenCalledTimes(1); + test('should call callback', () => { + expect(originalCallback).toHaveBeenCalledTimes(1); }); }); - describe('report with empty callback', async () => { + describe('report with empty callback', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reporter = createMockReporterInstance(); - await thundraWrapper.report(null, null, null); + + beforeAll((done) => { + thundraWrapper.report(null, null, null).then(done); + }); it('should not fail', () => { expect(thundraWrapper.reported).toBeTruthy(); }); }); describe('original function throws an error', () => { + let gotErr = undefined; const thrownError = 'err'; + const monitoringDisabled = false; const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => { - throw(thrownError); + const originalFunction = jest.fn((e, c, cb) => { + cb(thrownError, null); }); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); - thundraWrapper.report = jest.fn(); - try { - thundraWrapper.invoke(); - } catch (e) { - } + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); + thundraWrapper.reporter = createMockReporterInstance(); + + beforeAll(done => { + thundraWrapper.invoke().then(done, (err) => { + gotErr = err; + done(); + }); + }); + it('should call original function and report', () => { - expect(originalFunction.mock.calls.length).toBe(1); - expect(thundraWrapper.report).toBeCalledWith(thrownError, null, null); + expect(originalFunction).toHaveBeenCalledTimes(1); + expect(gotErr).toEqual(thrownError); }); }); @@ -78,11 +91,11 @@ describe('ThundraWrapper', () => { describe('timeout', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(); - jest.useFakeTimers(); + const originalFunction = jest.fn((event, context, callback) => {callback(null, 'hey')}); + const monitoringDisabled = false; originalContext.getRemainingTimeInMillis = () => 5000; pluginContext.timeoutMargin = 200; - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.report = jest.fn(); jest.runAllTimers(); originalContext.getRemainingTimeInMillis = null; @@ -98,8 +111,9 @@ describe('ThundraWrapper', () => { describe('with mock report function', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => compareBuild()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.report = jest.fn(); thundraWrapper.wrappedCallback(); it('should call report', () => { @@ -109,8 +123,9 @@ describe('ThundraWrapper', () => { describe('with real report function', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reporter = createMockReporterInstance(); thundraWrapper.wrappedCallback(); it('should call reporter.sendReports', () => { @@ -122,41 +137,48 @@ describe('ThundraWrapper', () => { describe('report once', async () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reported = true; thundraWrapper.reporter = createMockReporterInstance(); - await thundraWrapper.report(null, {result: 'result'}, thundraWrapper.originalCallback); + + beforeAll(done => { + thundraWrapper.report(null, {result: 'result'}, thundraWrapper.originalCallback).then(done); + }); it('should not send reports', () => { - expect(thundraWrapper.reporter.sendReports.mock.call.length).toBe(0); + expect(originalCallback).not.toHaveBeenCalled(); }); it('should not call callback', () => { - expect(originalCallback.mock.call.length).toBe(0); + expect(originalCallback).not.toHaveBeenCalled(); }); }); describe('AWS Lambda Proxy Response', async () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.executeHook = jest.fn(); const response = {statusCode: 500, body:'{\'message\':\'I have failed\'}'}; thundraWrapper.wrappedCallback(null, response); + it('should extract error from response with valid error response', () => { const expectedAfterInvocationData = { error: new HttpError('Lambda returned with error response.'), originalEvent, - response, + response, }; expect(thundraWrapper.executeHook).toBeCalledWith('after-invocation', expectedAfterInvocationData, true); }); - + }); describe('AWS Lambda Proxy Response', async () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.executeHook = jest.fn(); const response = {statusCode: 500, body:{ message: 'I have failed'}}; thundraWrapper.wrappedCallback(null, response); @@ -172,8 +194,9 @@ describe('ThundraWrapper', () => { describe('AWS Lambda Proxy Response', async () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.executeHook = jest.fn(); thundraWrapper.wrappedCallback(null, {statusCode: 200, body:'{\'message\':\'I have failed\'}'}); it('should extract error from response with success status', () => { @@ -184,16 +207,18 @@ describe('ThundraWrapper', () => { }; expect(thundraWrapper.executeHook).toBeCalledWith('after-invocation', expectedAfterInvocationData, true); }); - + }); describe('invoke', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reporter = createMockReporterInstance(); - thundraWrapper.invoke(); - it('should call original function and callback once', () => { + + it('should call original function and callback once', async () => { + await thundraWrapper.invoke(); expect(originalFunction.mock.calls.length).toBe(1); expect(originalCallback.mock.calls.length).toBe(1); }); @@ -208,7 +233,8 @@ describe('ThundraWrapper', () => { const originalFunction = jest.fn(); const originalCallback = null; const originalContext = createMockContext(); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const monitoringDisabled = false; + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reporter = createMockReporterInstance(); describe('succeed', () => { @@ -241,15 +267,15 @@ describe('ThundraWrapper', () => { describe('invoke', () => { const originalContext = createMockContext(); const originalCallback = null; - const originalFunction = jest.fn((event, context) => context.succeed()); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const monitoringDisabled = false; + const originalFunction = jest.fn((e, c) => c.succeed()); + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.wrappedContext.succeed = jest.fn(); - thundraWrapper.renport = jest.fn(); thundraWrapper.invoke(); - it('should call original function and wrappedContext\'s succeed', () => { - expect(originalFunction.mock.calls.length).toBe(1); - expect(thundraWrapper.wrappedContext.succeed.mock.calls.length).toBe(1); + it('should call original function and wrappedContext\'s succeed', async () => { + expect(originalFunction).toHaveBeenCalledTimes(1); + expect(thundraWrapper.wrappedContext.succeed).toHaveBeenCalledTimes(1); }); }); @@ -258,10 +284,15 @@ describe('ThundraWrapper', () => { describe('originalFunction returns promise', () => { const mockPromise = createMockPromise(); const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => mockPromise); - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, apiKey); + const monitoringDisabled = false; + const originalFunction = jest.fn((e, c) => mockPromise); + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pluginContext, monitoringDisabled); thundraWrapper.reporter = createMockReporterInstance(); - thundraWrapper.invoke(); + + beforeAll(async (done) => { + thundraWrapper.invoke().then(() => done()); + }); + it('should call originalCallback', () => { expect(originalContext.succeed).toBeCalledWith('test'); }); @@ -273,15 +304,15 @@ describe('ThundraWrapper', () => { describe('timeout sampling configured and TimeutError occurs', () => { const originalCallback = jest.fn(); const originalFunction = jest.fn(); + const monitoringDisabled = false; const pc = createMockPluginContext(); - jest.useFakeTimers(); originalContext.getRemainingTimeInMillis = () => 5000; pc.timeoutMargin = 200; pc.config = { sampleTimedOutInvocations: true }; - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pc, apiKey); + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pc, monitoringDisabled); thundraWrapper.executeAfteInvocationAndReport = jest.fn(); jest.runAllTimers(); originalContext.getRemainingTimeInMillis = null; @@ -297,22 +328,74 @@ describe('ThundraWrapper', () => { }); }); - describe('timeout sampling configured and TimeutError does not occur', () => { + describe('timeout sampling configured and TimeoutError does not occur', () => { const originalCallback = jest.fn(); - const originalFunction = jest.fn(() => originalCallback()); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; const pc = createMockPluginContext(); pc.config = { sampleTimedOutInvocations: true }; originalContext.getRemainingTimeInMillis = () => 0; - const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pc, apiKey); + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pc, monitoringDisabled); thundraWrapper.executeAfteInvocationAndReport = jest.fn(); thundraWrapper.report(null, {result: 'result'}, thundraWrapper.originalCallback); - + test('should not send reports', () => { expect(thundraWrapper.executeAfteInvocationAndReport).not.toBeCalled(); }); }); + + describe('should correctly decide to init debugger', () => { + const originalCallback = jest.fn(); + const originalFunction = jest.fn((e, c, cb) => cb()); + const monitoringDisabled = false; + const pc = createMockPluginContext(); + + const thundraWrapper = new ThundraWrapper(originalThis, originalEvent, originalContext, originalCallback, originalFunction, plugins, pc, monitoringDisabled); + + test('when debugger disabled and no token', () => { + delete process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN]; + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE] = 'false'; + + expect(thundraWrapper.shouldInitDebugger()).toBeFalsy(); + }); + + test('when debugger enabled and no token', () => { + delete process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN]; + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE] = 'true'; + + expect(thundraWrapper.shouldInitDebugger()).toBeFalsy(); + }); + + test('when debugger disabled and token exists', () => { + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN] = 'foobar'; + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE] = 'false'; + + expect(thundraWrapper.shouldInitDebugger()).toBeFalsy(); + }); + + test('when no token and no enable setting exist', () => { + delete process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN]; + delete process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE]; + + expect(thundraWrapper.shouldInitDebugger()).toBeFalsy(); + }); + + test('when token exists and no enable setting exists', () => { + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN] = 'foobar'; + delete process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE]; + + expect(thundraWrapper.shouldInitDebugger()).toBeTruthy(); + }); + + test('when token and enable setting exist', () => { + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_AUTH_TOKEN] = 'foobar'; + process.env[envVariableKeys.THUNDRA_AGENT_LAMBDA_DEBUGGER_ENABLE] = 'true'; + + expect(thundraWrapper.shouldInitDebugger()).toBeTruthy(); + }); + }); });