diff --git a/package.json b/package.json index 9d157f10..b55ae55b 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ }, "dependencies": { "@thundra/instrumenter": "^1.3.6", + "@thundra/slsdebugger": "^0.0.1", "@thundra/warmup": "^1.1.2", "fast-copy": "^2.1.0", "isomorphic-git": "^1.11.1", diff --git a/rollup.config.js b/rollup.config.js index d445598a..ea5bdbd1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -49,28 +49,6 @@ 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 d0a81756..777de9af 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -807,7 +807,6 @@ export const StdErrorLogContext = 'STDERR'; export const DefaultMongoCommandSizeLimit = 128 * 1024; -export const DEBUG_BRIDGE_FILE_NAME = 'debugBridge.js'; export const BROKER_WS_PROTOCOL = 'ws://'; export const BROKER_WSS_PROTOCOL = 'wss://'; export const BROKER_WS_HTTP_ERROR_PATTERN = /:\s*\D*(\d+)/; diff --git a/src/adaptor/slsdebugger/index.ts b/src/adaptor/slsdebugger/index.ts new file mode 100644 index 00000000..978ca547 --- /dev/null +++ b/src/adaptor/slsdebugger/index.ts @@ -0,0 +1,19 @@ +const SLSDebugger = require('@thundra/slsdebugger'); + +let initiated = false; +export const initiate = async () => { + if (!initiated) { + SLSDebugger.init(); + initiated = true; + } +} + +export const get = (additionalInfo : { [key: string]: any }) => { + if (!initiated) { + initiate(); + } + + return SLSDebugger._getSLSDebuggerInstance(additionalInfo); +} + + \ No newline at end of file diff --git a/src/config/ConfigNames.ts b/src/config/ConfigNames.ts index 718a79ed..a49a9763 100644 --- a/src/config/ConfigNames.ts +++ b/src/config/ConfigNames.ts @@ -227,24 +227,8 @@ class ConfigNames { ///////////////////////////////////////////////////////////////////////////// - public static readonly THUNDRA_LAMBDA_DEBUGGER_ENABLE: string = - 'thundra.agent.lambda.debugger.enable'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_PORT: string = - 'thundra.agent.lambda.debugger.port'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_LOGS_ENABLE: string = - 'thundra.agent.lambda.debugger.logs.enable'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_WAIT_MAX: string = - 'thundra.agent.lambda.debugger.wait.max'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_IO_WAIT: string = - 'thundra.agent.lambda.debugger.io.wait'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_BROKER_PORT: string = - 'thundra.agent.lambda.debugger.broker.port'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_BROKER_HOST: string = - 'thundra.agent.lambda.debugger.broker.host'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_SESSION_NAME: string = - 'thundra.agent.lambda.debugger.session.name'; - public static readonly THUNDRA_LAMBDA_DEBUGGER_AUTH_TOKEN: string = - 'thundra.agent.lambda.debugger.auth.token'; + public static readonly SLSDEBUGGER_AUTH_TOKEN: string = + 'SLSDEBUGGER_AUTH_TOKEN'; ///////////////////////////////////////////////////////////////////////////// diff --git a/src/debugBridge.ts b/src/debugBridge.ts deleted file mode 100644 index 964eec28..00000000 --- a/src/debugBridge.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Connects Node.js {@code Inspector} at application side here and - * Debugger at user/developer side over Thundra Debug Broker. - */ - -const net = require('net'); -const WebSocket = require('ws'); -const { DEBUGGER_PORT, BROKER_HOST, BROKER_PORT, - LOGS_ENABLED, AUTH_TOKEN, SESSION_NAME, SESSION_TIMEOUT, BROKER_PROTOCOL } = process.env; - -const CLOSING_CODES: {[key: string]: number} = { - NORMAL: 1000, - TIMEOUT: 4000, -}; - -const RUNTIME = 'node'; -const PROTOCOL_VERSION = '1.0'; - -const log = (...params: any[]) => { - if (LOGS_ENABLED === 'true') { - console.log(...params); - } -}; - -const debuggerSocket = new net.Socket(); -const brokerSocket = new WebSocket( - `${BROKER_PROTOCOL}${BROKER_HOST}:${BROKER_PORT}`, - { - headers: { - 'x-thundra-auth-token': AUTH_TOKEN, - 'x-thundra-session-name': SESSION_NAME, - 'x-thundra-protocol-version': PROTOCOL_VERSION, - 'x-thundra-session-timeout': SESSION_TIMEOUT, - 'x-thundra-runtime': RUNTIME, - }, - followRedirects: true, - }, -); - -// Setup debugger socket -debuggerSocket.on('data', (data: Buffer) => { - brokerSocket.send(data); -}); -debuggerSocket.on('end', () => { - log('debuggerSocket: disconnected from the main 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 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/wrappers/lambda/LambdaHandlerWrapper.ts b/src/wrappers/lambda/LambdaHandlerWrapper.ts index 27f83009..237e123c 100644 --- a/src/wrappers/lambda/LambdaHandlerWrapper.ts +++ b/src/wrappers/lambda/LambdaHandlerWrapper.ts @@ -4,23 +4,12 @@ import HttpError from '../../error/HttpError'; import ThundraConfig from '../../plugins/config/ThundraConfig'; import PluginContext from '../../plugins/PluginContext'; import ThundraLogger from '../../ThundraLogger'; -import { - BROKER_WS_HTTP_ERR_CODE_TO_MSG, - BROKER_WS_HTTP_ERROR_PATTERN, - BROKER_WS_PROTOCOL, - BROKER_WSS_PROTOCOL, - DEBUG_BRIDGE_FILE_NAME, -} from '../../Constants'; import Utils from '../../utils/Utils'; -import { readFileSync } from 'fs'; -import ConfigProvider from '../../config/ConfigProvider'; -import ConfigNames from '../../config/ConfigNames'; import ExecutionContextManager from '../../context/ExecutionContextManager'; import ExecutionContext from '../../context/ExecutionContext'; import HTTPUtils from '../../utils/HTTPUtils'; import LambdaUtils from '../../utils/LambdaUtils'; - -const path = require('path'); +import SLSDebugger from '@thundra/slsdebugger/dist/debugger/SlsDebugger'; /** * Wraps the Lambda handler function. @@ -48,22 +37,10 @@ class LambdaHandlerWrapper { private timeout: NodeJS.Timer; private resolve: any; private reject: any; - private inspector: any; - private fork: any; - private debuggerPort: number; - private debuggerMaxWaitTime: number; - private debuggerIOWaitTime: number; - private brokerHost: string; - private sessionName: string; - private brokerProtocol: string; - private authToken: string; - private sessionTimeout: number; - private brokerPort: number; - private debuggerProxy: any; - private debuggerLogsEnabled: boolean; + private slsDebugger?: SLSDebugger; constructor(self: any, event: any, context: any, callback: any, originalFunction: any, - plugins: any, pluginContext: PluginContext, config: ThundraConfig) { + plugins: any, pluginContext: PluginContext, config: ThundraConfig, slsDebugger?: SLSDebugger) { this.originalThis = self; this.originalEvent = event; this.originalContext = context; @@ -75,6 +52,7 @@ class LambdaHandlerWrapper { this.pluginContext.maxMemory = parseInt(context.memoryLimitInMB, 10); this.completed = false; this.reporter = new Reporter(this.config.apiKey); + this.slsDebugger = slsDebugger; this.wrappedContext = { ...context, done: (error: any, result: any) => { @@ -104,10 +82,6 @@ class LambdaHandlerWrapper { return me.originalContext.callbackWaitsForEmptyEventLoop; }, }, this.wrappedContext); - - if (this.shouldInitDebugger()) { - this.initDebugger(); - } } // Invocation related stuff // @@ -122,7 +96,9 @@ class LambdaHandlerWrapper { this.config.refreshConfig(); - await this.startDebuggerProxyIfAvailable(); + if (this.slsDebugger) { + await this.slsDebugger.start(); + } this.resolve = undefined; this.reject = undefined; @@ -244,7 +220,9 @@ class LambdaHandlerWrapper { ThundraLogger.debug(' Failed to report:', e); } - this.finishDebuggerProxyIfAvailable(); + if (this.slsDebugger) { + this.slsDebugger.close(); + } } finally { if (!timeout) { this.onFinish(error, result); @@ -329,246 +307,6 @@ class LambdaHandlerWrapper { ////////////////////////////////////////////////////////////////////////////////////////////////////// - // Debugger related stuff // - ////////////////////////////////////////////////////////////////////////////////////////////////////// - - private shouldInitDebugger(): boolean { - const authToken = ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_AUTH_TOKEN); - const debuggerEnable = ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_ENABLE, null); - if (debuggerEnable != null) { - return debuggerEnable && authToken !== undefined; - } else { - return authToken !== undefined; - } - } - - private initDebugger(): void { - try { - this.inspector = require('inspector'); - this.fork = require('child_process').fork; - - const debuggerPort = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_PORT); - const brokerHost = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_BROKER_HOST); - const brokerPort = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_BROKER_PORT); - const authToken = - ConfigProvider.get( - ConfigNames.THUNDRA_LAMBDA_DEBUGGER_AUTH_TOKEN, - ''); - const sessionName = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_SESSION_NAME); - const debuggerMaxWaitTime = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_WAIT_MAX); - const debuggerIOWaitTime = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_IO_WAIT); - const debuggerLogsEnabled = - ConfigProvider.get(ConfigNames.THUNDRA_LAMBDA_DEBUGGER_LOGS_ENABLE); - let brokerProtocol = BROKER_WSS_PROTOCOL; - - if (brokerHost.startsWith(BROKER_WS_PROTOCOL) || brokerHost.startsWith(BROKER_WSS_PROTOCOL)) { - // If WebSocket protocol is already included in the broker address, do not add protocol string - brokerProtocol = ''; - } - - if (ThundraLogger.isDebugEnabled()) { - ThundraLogger.debug(' Initializing debugger with the following configurations:', { - debuggerPort, debuggerMaxWaitTime, debuggerIOWaitTime, - brokerProtocol, brokerHost, brokerPort, - sessionName, authToken, debuggerLogsEnabled, - }); - } - - if (brokerPort === -1) { - throw new Error( - 'For debugging, you must set debug broker port through \ - \'thundra_agent_lambda_debug_broker_port\' environment variable'); - } - - this.debuggerPort = debuggerPort; - this.debuggerMaxWaitTime = debuggerMaxWaitTime; - this.debuggerIOWaitTime = debuggerIOWaitTime; - this.brokerProtocol = brokerProtocol; - 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.fork = null; - this.inspector = null; - } - } - - private getDebuggerProxyIOMetrics(): any { - try { - const ioContent = readFileSync('/proc/' + this.debuggerProxy.pid + '/io', 'utf8'); - const ioMetrics = ioContent.split('\n'); - return { - rchar: ioMetrics[0], - wchar: ioMetrics[1], - }; - } catch (e) { - return null; - } - } - - private async waitForDebugger() { - let prevRchar = 0; - let prevWchar = 0; - let initCompleted = false; - ThundraLogger.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 (ThundraLogger.isDebugEnabled()) { - ThundraLogger.debug( - ' Got debugger proxy process IO metrics:', debuggerIoMetrics); - } - if (!debuggerIoMetrics) { - await sleep(this.debuggerIOWaitTime); - ThundraLogger.debug( - ' No IO metrics was able to detected for debugger proxy process, \ - finished waiting for handshake'); - break; - } - if (ThundraLogger.isDebugEnabled()) { - ThundraLogger.debug(' Checking debugger proxy process IO metrics:', { - prevRchar, prevWchar, - rchar: debuggerIoMetrics.rchar, wchar: debuggerIoMetrics.wchar, - }); - } - if (prevRchar !== 0 && prevWchar !== 0 && - debuggerIoMetrics.rchar === prevRchar && debuggerIoMetrics.wchar === prevWchar) { - ThundraLogger.debug( - ' Debugger proxy process is idle since latest check, \ - finished waiting for handshake'); - initCompleted = true; - break; - } - prevRchar = debuggerIoMetrics.rchar; - prevWchar = debuggerIoMetrics.wchar; - } catch (e) { - ThundraLogger.error( - ' Error occurred while waiting debugger handshake to complete:', e); - break; - } - await sleep(this.debuggerIOWaitTime); - } - if (initCompleted) { - ThundraLogger.info(' Completed debugger handshake'); - } else { - ThundraLogger.error( - ' Couldn\'t complete debugger handshake in ' + this.debuggerMaxWaitTime + ' milliseconds.'); - } - } - - private async startDebuggerProxyIfAvailable() { - ThundraLogger.debug(' Start debugger proxy if available'); - if (this.debuggerProxy) { - this.finishDebuggerProxyIfAvailable(); - } - if (this.fork && this.inspector) { - try { - ThundraLogger.debug(' Forking debugger proxy process'); - 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_PROTOCOL: this.brokerProtocol, - }, - }, - ); - - ThundraLogger.debug(' Opening inspector'); - 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.error( - ' Received error message from Thundra Debugger:', errMessage); - - return resolve(true); - }); - }); - - ThundraLogger.debug(' Waiting for broker connection ...'); - const brokerHasErr = await waitForBrokerConnection(); - - if (brokerHasErr) { - ThundraLogger.debug(' Failed waiting for broker connection'); - this.finishDebuggerProxyIfAvailable(); - return; - } - - await this.waitForDebugger(); - } catch (e) { - this.debuggerProxy = null; - ThundraLogger.error(' Error occurred while starting debugger proxy:', e); - } - } - } - - private finishDebuggerProxyIfAvailable(): void { - ThundraLogger.debug(' Finish debugger proxy if available'); - try { - if (this.inspector) { - ThundraLogger.debug(' Closing inspector'); - this.inspector.close(); - this.inspector = null; - } - } catch (e) { - ThundraLogger.error( - ' Error occurred while finishing debugger proxy:', e); - } - if (this.debuggerProxy) { - try { - if (!this.debuggerProxy.killed) { - ThundraLogger.debug(' Killing debugger proxy process'); - this.debuggerProxy.kill(); - } - } catch (e) { - ThundraLogger.error( - ' Error occurred while killing debugger proxy process:', e); - } finally { - this.debuggerProxy = null; - } - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////// - // Timeout related stuff // ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -594,18 +332,8 @@ class LambdaHandlerWrapper { return setTimeout(() => { ThundraLogger.debug(' Detected timeout'); - if (this.debuggerProxy) { - // Debugger proxy exists, let it know about the timeout - try { - if (!this.debuggerProxy.killed) { - ThundraLogger.debug(' Killing debugger proxy process on timeout'); - this.debuggerProxy.kill('SIGHUP'); - } - } catch (e) { - ThundraLogger.error(' Error occurred while killing debugger proxy:', e); - } finally { - this.debuggerProxy = null; - } + if (this.slsDebugger) { + this.slsDebugger.kill(); } ThundraLogger.debug(' Reporting timeout error'); return this.completeInvocation(new TimeoutError('Lambda is timed out.'), null, true); diff --git a/src/wrappers/lambda/LambdaWrapper.ts b/src/wrappers/lambda/LambdaWrapper.ts index 25931d09..b6788662 100644 --- a/src/wrappers/lambda/LambdaWrapper.ts +++ b/src/wrappers/lambda/LambdaWrapper.ts @@ -23,6 +23,7 @@ import ThundraTracer from '../../opentracing/Tracer'; import ExecutionContext from '../../context/ExecutionContext'; import { LambdaPlatformUtils } from './LambdaPlatformUtils'; import ThundraLogger from '../../ThundraLogger'; +import * as SLSDebuggerAdaptor from '../../adaptor/slsdebugger'; const get = require('lodash.get'); @@ -108,6 +109,8 @@ function createWrapperWithPlugins(config: ThundraConfig, */ function createWrappedHandler(pluginContext: PluginContext, originalFunc: Function, plugins: any[], config: ThundraConfig): WrappedFunction { + + const slsdebuggerEnable = process.env[ConfigNames.SLSDEBUGGER_AUTH_TOKEN]; const wrappedFunction = (originalEvent: any, originalContext: any, originalCallback: any) => ExecutionContextManager.runWithContext( @@ -118,6 +121,13 @@ function createWrappedHandler(pluginContext: PluginContext, originalFunc: Functi applicationId: LambdaPlatformUtils.getApplicationId(originalContext), }); + let slsDebugger; + if (slsdebuggerEnable) { + slsDebugger = SLSDebuggerAdaptor.get({ + remainingTimeInMillis: originalContext.getRemainingTimeInMillis() + }); + } + const thundraWrapper = new LambdaHandlerWrapper( this, originalEvent, @@ -127,6 +137,7 @@ function createWrappedHandler(pluginContext: PluginContext, originalFunc: Functi plugins, pluginContext, config, + slsDebugger, ); return await thundraWrapper.invoke(); },