@instana/serverless
Advanced tools
+11
-0
@@ -6,2 +6,13 @@ # Change Log | ||
| # [4.21.0](https://github.com/instana/nodejs/compare/v4.20.0...v4.21.0) (2025-07-31) | ||
| ### Features | ||
| * **aws-lambda:** improved overhaul performance ([#1315](https://github.com/instana/nodejs/issues/1315)) ([4620113](https://github.com/instana/nodejs/commit/46201132dac6a73e7719d085c4edaa5c5a5ae526)) | ||
| # [4.20.0](https://github.com/instana/nodejs/compare/v4.19.1...v4.20.0) (2025-07-30) | ||
@@ -8,0 +19,0 @@ |
+3
-3
| { | ||
| "name": "@instana/serverless", | ||
| "version": "4.20.0", | ||
| "version": "4.21.0", | ||
| "description": "Internal utility package for serverless Node.js tracing and monitoring with Instana", | ||
@@ -68,7 +68,7 @@ "author": { | ||
| "dependencies": { | ||
| "@instana/core": "4.20.0", | ||
| "@instana/core": "4.21.0", | ||
| "agent-base": "^6.0.2", | ||
| "https-proxy-agent": "^7.0.2" | ||
| }, | ||
| "gitHead": "6b4956a569bc7fb5437028e20896bab3da2143a4" | ||
| "gitHead": "356f582d62210fb69f7174a417ab70ad7c21c3e7" | ||
| } |
+200
-123
@@ -12,3 +12,2 @@ /* | ||
| const constants = require('./constants'); | ||
| const layerExtensionHostname = 'localhost'; | ||
@@ -20,3 +19,15 @@ const layerExtensionPort = process.env.INSTANA_LAYER_EXTENSION_PORT | ||
| const timeoutEnvVar = 'INSTANA_TIMEOUT'; | ||
| const layerExtensionTimeout = process.env.INSTANA_LAMBDA_EXTENSION_TIMEOUT_IN_MS | ||
| // NOTE: The heartbeat is usually really, really fast (<30ms). | ||
| const layerExtensionHeartbeatTimeout = 100; | ||
| // NOTE: The initial heartbeat can be very slow when the Lambda is in cold start. | ||
| const initialLayerExtensionHeartbeatTimeout = 2000; | ||
| // NOTE: When lambda is in cold start, the communication between the handler | ||
| // and the extension can take a while. We need to have a bigger timeout | ||
| // for the initially. | ||
| const initialLayerExtensionRequestTimeout = 2000; | ||
| const layerExtensionRequestTimeout = process.env.INSTANA_LAMBDA_EXTENSION_TIMEOUT_IN_MS | ||
| ? Number(process.env.INSTANA_LAMBDA_EXTENSION_TIMEOUT_IN_MS) | ||
@@ -26,9 +37,7 @@ : 500; | ||
| const proxyEnvVar = 'INSTANA_ENDPOINT_PROXY'; | ||
| let proxyAgent; | ||
| const disableCaCheckEnvVar = 'INSTANA_DISABLE_CA_CHECK'; | ||
| const disableCaCheck = process.env[disableCaCheckEnvVar] === 'true'; | ||
| let requestHasFailed = false; | ||
| let proxyAgent; | ||
| let warningsHaveBeenLogged = false; | ||
| let firstRequestToExtension = true; | ||
@@ -38,5 +47,6 @@ const defaults = { | ||
| identityProvider: null, | ||
| stopSendingOnFailure: true, | ||
| isLambdaRequest: false, | ||
| backendTimeout: 500, | ||
| useLambdaExtension: false | ||
| useLambdaExtension: false, | ||
| retries: false | ||
| }; | ||
@@ -69,2 +79,4 @@ | ||
| const requests = {}; | ||
| exports.init = function init(opts) { | ||
@@ -126,14 +138,7 @@ options = Object.assign(defaults, opts); | ||
| exports.setLogger = function setLogger(_logger) { | ||
| logger = _logger; | ||
| }; | ||
| /** | ||
| * | ||
| * "finalLambdaRequest": | ||
| * When using AWS Lambda, we send metrics and spans together | ||
| * using the function "sendBundle". The variable was invented to indicate | ||
| * that this is the last request to be sent before the AWS Lambda runtime might freeze the process. | ||
| * Currently, there is exactly one request to send all the data and | ||
| * the variable is always true. | ||
| * using the function "sendBundle" at the end of the invocation - before the AWS Lambda | ||
| * runtime might freeze the process. The span buffer sends data reguarly using `sendSpans`. | ||
| */ | ||
@@ -159,8 +164,14 @@ exports.sendBundle = function sendBundle(bundle, finalLambdaRequest, callback) { | ||
| let heartbeatInterval; | ||
| let heartbeatIsActive = false; | ||
| function scheduleLambdaExtensionHeartbeatRequest() { | ||
| const executeHeartbeat = () => { | ||
| const executeHeartbeat = (heartbeatOpts = {}) => { | ||
| if (heartbeatIsActive) { | ||
| return; | ||
| } | ||
| const startTime = Date.now(); | ||
| const requestId = getRequestId(); | ||
| const startTime = Date.now(); | ||
| logger.debug(`[${requestId}] Executing Heartbeat request to Lambda extension.`); | ||
| heartbeatIsActive = true; | ||
@@ -173,17 +184,17 @@ const req = uninstrumented.http.request( | ||
| method: 'POST', | ||
| Connection: 'close', | ||
| // This sets a timeout for establishing the socket connection, see setTimeout below for a timeout for an | ||
| // idle connection after the socket has been opened. | ||
| timeout: layerExtensionTimeout | ||
| headers: { | ||
| Connection: 'keep-alive' | ||
| } | ||
| }, | ||
| res => { | ||
| logger.debug(`[${requestId}] Took ${Date.now() - startTime} ms to send heartbeat to the extension.`); | ||
| if (res.statusCode === 200) { | ||
| logger.debug(`[${requestId}] The Instana Lambda extension Heartbeat request has succeeded.`); | ||
| logger.debug(`[${requestId}] The Instana Lambda extension heartbeat request has succeeded.`); | ||
| } else { | ||
| handleHeartbeatError( | ||
| new Error( | ||
| `[${requestId}] The Instana Lambda extension Heartbeat request has ` + | ||
| `returned an unexpected status code: ${res.statusCode}.` | ||
| ) | ||
| logger.debug( | ||
| `[${requestId}] The Instana Lambda extension heartbeat request has failed. Status Code: ${res.statusCode}` | ||
| ); | ||
| handleHeartbeatError(); | ||
| } | ||
@@ -193,2 +204,3 @@ | ||
| // we need to register the handlers to avoid running into a timeout | ||
| // because the request expects to receive body data | ||
| }); | ||
@@ -200,2 +212,4 @@ | ||
| logger.debug(`[${requestId}] Took ${duration}ms to receive response from extension`); | ||
| heartbeatIsActive = false; | ||
| }); | ||
@@ -205,21 +219,17 @@ } | ||
| req.once('finish', () => { | ||
| const endTime = Date.now(); | ||
| const duration = endTime - startTime; | ||
| logger.debug(`[${requestId}] Took ${duration}ms to send data to extension`); | ||
| req.once('error', e => { | ||
| logger.debug(`[${requestId}] The Heartbeat request did not succeed.`, e); | ||
| // req.destroyed indicates that we have run into a timeout and have | ||
| // already handled the timeout error. | ||
| if (req.destroyed) { | ||
| return; | ||
| } | ||
| handleHeartbeatError(); | ||
| }); | ||
| function handleHeartbeatError(e) { | ||
| // Make sure we do not try to talk to the Lambda extension again. | ||
| options.useLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| req.setTimeout(heartbeatOpts.heartbeatTimeout, () => { | ||
| logger.debug(`[${requestId}] Heartbeat request timed out.`); | ||
| logger.debug( | ||
| `[${requestId}] The Instana Lambda extension Heartbeat request did not succeed. ` + | ||
| 'Falling back to talking to the Instana back ' + | ||
| `end directly. ${e?.message} ${e?.stack}` | ||
| ); | ||
| } | ||
| req.once('error', e => { | ||
| // req.destroyed indicates that we have run into a timeout and have already handled the timeout error. | ||
@@ -230,11 +240,13 @@ if (req.destroyed) { | ||
| handleHeartbeatError(e); | ||
| handleHeartbeatError(); | ||
| }); | ||
| // Handle timeouts that occur after connecting to the socket (no response from the extension). | ||
| req.setTimeout(layerExtensionTimeout, () => { | ||
| handleHeartbeatError(new Error(`[${requestId}] The Lambda extension Heartbeat request timed out.`)); | ||
| function handleHeartbeatError() { | ||
| logger.warn(`[${requestId}] Heartbeat request failed. Falling back to the serverless acceptor instead.`); | ||
| destroyRequest(req); | ||
| }); | ||
| options.useLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| cleanupRequest(req); | ||
| heartbeatIsActive = false; | ||
| } | ||
@@ -245,11 +257,15 @@ req.end(); | ||
| // call immediately | ||
| executeHeartbeat(); | ||
| // timeout is bigger because of possible coldstart | ||
| executeHeartbeat({ heartbeatTimeout: initialLayerExtensionHeartbeatTimeout }); | ||
| // NOTE: it is fine to use interval, because the req timeout is 300ms and the interval is 500 | ||
| heartbeatInterval = setInterval(executeHeartbeat, 500); | ||
| heartbeatInterval = setInterval(() => { | ||
| logger.debug('Heartbeat interval is alive.'); | ||
| executeHeartbeat({ heartbeatTimeout: layerExtensionHeartbeatTimeout }); | ||
| }, 300); | ||
| heartbeatInterval.unref(); | ||
| } | ||
| function getTransport(localUseLambdaExtension) { | ||
| if (localUseLambdaExtension) { | ||
| function getTransport() { | ||
| if (options.useLambdaExtension) { | ||
| // The Lambda extension is always HTTP without TLS on localhost. | ||
@@ -262,7 +278,16 @@ return uninstrumented.http; | ||
| function getBackendTimeout(localUseLambdaExtension) { | ||
| return localUseLambdaExtension ? layerExtensionTimeout : options.backendTimeout; | ||
| function getBackendTimeout() { | ||
| if (options.useLambdaExtension) { | ||
| if (firstRequestToExtension) { | ||
| firstRequestToExtension = false; | ||
| return initialLayerExtensionRequestTimeout; | ||
| } else { | ||
| return layerExtensionRequestTimeout; | ||
| } | ||
| } | ||
| return options.backendTimeout; | ||
| } | ||
| function send({ resourcePath, payload, finalLambdaRequest, callback, requestId }) { | ||
| function send({ resourcePath, payload, finalLambdaRequest, callback, tries, requestId }) { | ||
| let callbackWasCalled = false; | ||
@@ -275,15 +300,4 @@ const handleCallback = args => { | ||
| // We need a local copy of the global useLambdaExtension variable, otherwise it might be changed concurrently by | ||
| // scheduleLambdaExtensionHeartbeatRequest. But we need to remember the value at the time we _started_ the request to | ||
| // decide whether to fall back to sending to the back end directly or give up sending data completely. | ||
| let localUseLambdaExtension = options.useLambdaExtension; | ||
| if (requestHasFailed && options.stopSendingOnFailure) { | ||
| logger.info( | ||
| `[${requestId}] Not attempting to send data to ${resourcePath} as a previous request ` + | ||
| 'has already timed out or failed.' | ||
| ); | ||
| handleCallback(); | ||
| return; | ||
| if (tries === undefined) { | ||
| tries = 0; | ||
| } | ||
@@ -315,3 +329,3 @@ | ||
| const requestPath = | ||
| localUseLambdaExtension || environmentUtil.getBackendPath() === '/' | ||
| options.useLambdaExtension || environmentUtil.getBackendPath() === '/' | ||
| ? resourcePath | ||
@@ -324,4 +338,4 @@ : environmentUtil.getBackendPath() + resourcePath; | ||
| const reqOptions = { | ||
| hostname: localUseLambdaExtension ? layerExtensionHostname : environmentUtil.getBackendHost(), | ||
| port: localUseLambdaExtension ? layerExtensionPort : environmentUtil.getBackendPort(), | ||
| hostname: options.useLambdaExtension ? layerExtensionHostname : environmentUtil.getBackendHost(), | ||
| port: options.useLambdaExtension ? layerExtensionPort : environmentUtil.getBackendPort(), | ||
| path: requestPath, | ||
@@ -332,2 +346,3 @@ method: 'POST', | ||
| 'Content-Length': Buffer.byteLength(serializedPayload), | ||
| Connection: 'keep-alive', | ||
| [constants.xInstanaHost]: hostHeader, | ||
@@ -344,5 +359,7 @@ [constants.xInstanaKey]: environmentUtil.getInstanaAgentKey() | ||
| reqOptions.timeout = getBackendTimeout(localUseLambdaExtension); | ||
| // This timeout is for **inactivity** - Backend sends no data at all | ||
| // So if the timeout is set to 500ms, it does not mean that the request will be aborted after 500ms | ||
| reqOptions.timeout = getBackendTimeout(options.useLambdaExtension); | ||
| if (proxyAgent && !localUseLambdaExtension) { | ||
| if (proxyAgent && !options.useLambdaExtension) { | ||
| reqOptions.agent = proxyAgent; | ||
@@ -352,4 +369,5 @@ } | ||
| let req; | ||
| const skipWaitingForHttpResponse = !proxyAgent && !localUseLambdaExtension; | ||
| const transport = getTransport(localUseLambdaExtension); | ||
| const skipWaitingForHttpResponse = !proxyAgent && !options.useLambdaExtension; | ||
| const transport = getTransport(options.useLambdaExtension); | ||
| const start = Date.now(); | ||
@@ -383,3 +401,3 @@ if (skipWaitingForHttpResponse) { | ||
| if (finalLambdaRequest) { | ||
| cleanupRequest(req); | ||
| cleanupRequests(); | ||
| } | ||
@@ -391,2 +409,22 @@ | ||
| if (options.isLambdaRequest) { | ||
| requests[requestId] = req; | ||
| } | ||
| req.on('response', res => { | ||
| const { statusCode } = res; | ||
| if (statusCode >= 200 && statusCode < 300) { | ||
| logger.debug(`${requestId} Received response from Instana (${requestPath}).`); | ||
| } else { | ||
| logger.debug(`${requestId} Received response from Instana has been failed (${requestPath}).`); | ||
| } | ||
| logger.debug(`[${requestId}] Received HTTP status code ${statusCode} from Instana (${requestPath}).`); | ||
| logger.debug(`[${requestId}] Sending and receiving data to Instana took: ${Date.now() - start} ms.`); | ||
| cleanupRequest(req); | ||
| delete requests[requestId]; | ||
| }); | ||
| // See above for the difference between the timeout attribute in the request options and handling the 'timeout' | ||
@@ -398,7 +436,19 @@ // event. This only adds a read timeout after the connection has been established and we need the timout attribute | ||
| // > socket.setTimeout() will be called. | ||
| req.on('timeout', () => | ||
| onTimeout(localUseLambdaExtension, req, resourcePath, payload, finalLambdaRequest, handleCallback, requestId) | ||
| ); | ||
| req.on('timeout', () => { | ||
| logger.debug(`[${requestId}] Timeout while sending data to Instana (${requestPath}).`); | ||
| if (options.isLambdaRequest) { | ||
| delete requests[requestId]; | ||
| } | ||
| onTimeout(req, resourcePath, payload, finalLambdaRequest, handleCallback, tries, requestId); | ||
| }); | ||
| req.on('error', e => { | ||
| logger.debug(`[${requestId}] Error while sending data to Instana (${requestPath}): ${e?.message} ${e?.stack}`); | ||
| if (options.isLambdaRequest) { | ||
| delete requests[requestId]; | ||
| } | ||
| // CASE: we manually destroy streams, skip these errors | ||
@@ -414,3 +464,3 @@ // Otherwise we will produce `Error: socket hang up` errors in the logs | ||
| if (localUseLambdaExtension) { | ||
| if (options.useLambdaExtension) { | ||
| // This is a failure from talking to the Lambda extension on localhost. Most probably it is simply not available | ||
@@ -421,22 +471,23 @@ // because @instana/aws-lambda has been installed as a normal npm dependency instead of using Instana's | ||
| // target in place. | ||
| logger.debug( | ||
| `[${requestId}] Could not connect to the Instana Lambda extension. ` + | ||
| `Falling back to talking to the Instana back end directly: ${e?.message} ${e?.stack}` | ||
| ); | ||
| logger.debug(`[${requestId}] Could not connect to the Instana Lambda extension (tries: ${tries}).`); | ||
| // Make sure we do not try to talk to the Lambda extension again. | ||
| options.useLambdaExtension = localUseLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| if (options.retries === false || tries >= 1) { | ||
| clearInterval(heartbeatInterval); | ||
| // Retry the request immediately, this time sending it to serverless-acceptor directly. | ||
| send({ resourcePath, payload, finalLambdaRequest, callback, requestId }); | ||
| // Retry the request immediately, this time sending it to serverless-acceptor directly. | ||
| logger.warn( | ||
| // eslint-disable-next-line max-len | ||
| `[${requestId}] Trying to send data to Instana serverless acceptor instead because the Lambda extension cannot be reached in time.` | ||
| ); | ||
| options.useLambdaExtension = false; | ||
| return send({ resourcePath, payload, finalLambdaRequest, callback, tries: 0, requestId }); | ||
| } | ||
| logger.debug(`[${requestId}] Retrying...`); | ||
| send({ resourcePath, payload, finalLambdaRequest, callback, tries: tries + 1, requestId }); | ||
| } else { | ||
| // We are not using the Lambda extension, because we are either not in an AWS Lambda, or a previous request to the | ||
| // extension has already failed. Thus, this is a failure from talking directly to serverless-acceptor | ||
| // (or a user-provided proxy). | ||
| requestHasFailed = true; | ||
| if (proxyAgent) { | ||
| logger.warn( | ||
| `[${requestId}] Could not send traces and metrics to Instana. Could not connect to the configured proxy ` + | ||
| `[${requestId}] Could not send trace data to ${resourcePath}. Could not connect to the configured proxy ` + | ||
| `${process.env[proxyEnvVar]}.` + | ||
@@ -447,3 +498,3 @@ `${e?.message} ${e?.stack}` | ||
| logger.warn( | ||
| `[${requestId}] Could not send traces and metrics to Instana. ` + | ||
| `[${requestId}] Could not send trace data to ${resourcePath}. ` + | ||
| `The Instana back end seems to be unavailable. ${e?.message} , ${e?.stack}` | ||
@@ -453,6 +504,13 @@ ); | ||
| handleCallback(e); | ||
| if (options.retries === false || tries >= 1) { | ||
| logger.debug(`[${requestId}] Giving up...`); | ||
| return handleCallback(e); | ||
| } | ||
| logger.debug(`[${requestId}] Retrying...`); | ||
| send({ resourcePath, payload, finalLambdaRequest, callback, tries: tries + 1, requestId }); | ||
| } | ||
| }); | ||
| // This only indicates that the request has been successfully send! Independent of the response! | ||
| req.on('finish', () => { | ||
@@ -470,5 +528,7 @@ logger.debug( | ||
| if (skipWaitingForHttpResponse) { | ||
| // NOTE: When the callback of `.end` is called, the data was successfully send to the server. | ||
| // That does not mean the server has responded in any way! | ||
| req.end(serializedPayload, () => { | ||
| if (finalLambdaRequest) { | ||
| cleanupRequest(req); | ||
| if (options.isLambdaRequest && finalLambdaRequest) { | ||
| cleanupRequests(); | ||
| } | ||
@@ -486,27 +546,30 @@ | ||
| function onTimeout(localUseLambdaExtension, req, resourcePath, payload, finalLambdaRequest, handleCallback, requestId) { | ||
| if (localUseLambdaExtension) { | ||
| function onTimeout(req, resourcePath, payload, finalLambdaRequest, handleCallback, tries, requestId) { | ||
| if (options.useLambdaExtension) { | ||
| // This is a timeout from talking to the Lambda extension on localhost. Most probably it is simply not available | ||
| // because @instana/aws-lambda has been installed as a normal npm dependency instead of using Instana's | ||
| // Lambda layer. We use this failure as a signal to not try to the extension again and instead fall back to | ||
| // talking to serverless-acceptor directly. We also immediately retry the current request with that new downstream | ||
| // talking to serverless acceptor directly. We also immediately retry the current request with that new downstream | ||
| // target in place. | ||
| logger.debug( | ||
| `[${requestId}] Request timed out while trying to talk to Instana Lambda extension. ` + | ||
| 'Falling back to talking to the Instana back end directly.' | ||
| ); | ||
| logger.debug(`[${requestId}] Request timed out while trying to talk to Instana Lambda extension.`); | ||
| // Make sure we do not try to talk to the Lambda extension again. | ||
| options.useLambdaExtension = localUseLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| destroyRequest(req); | ||
| cleanupRequest(req); | ||
| // Retry the request immediately, this time sending it to serverless-acceptor directly. | ||
| send({ resourcePath, payload, finalLambdaRequest, callback: handleCallback, requestId }); | ||
| // CASE: It could be that a parallel request or the heartbeat already set useLambdaExtension to false. | ||
| if (options.retries === false || tries >= 1) { | ||
| clearInterval(heartbeatInterval); | ||
| // Retry the request immediately, this time sending it to serverless acceptor directly. | ||
| logger.warn( | ||
| `[${requestId}] Trying to send data to Instana serverless acceptor instead because the Lambda extension ` + | ||
| 'cannot be reached in time.' | ||
| ); | ||
| options.useLambdaExtension = false; | ||
| return send({ resourcePath, payload, finalLambdaRequest, callback: handleCallback, tries: 0, requestId }); | ||
| } | ||
| logger.debug(`[${requestId}] Retrying...`); | ||
| send({ resourcePath, payload, finalLambdaRequest, callback: handleCallback, tries: tries + 1, requestId }); | ||
| } else { | ||
| // We are not using the Lambda extension, because we are either not in an AWS Lambda, or a previous request to the | ||
| // extension has already failed. Thus, this is a timeout from talking directly to serverless-acceptor | ||
| // (or a user-provided proxy). | ||
| requestHasFailed = true; | ||
| // We need to destroy the request manually, otherwise it keeps the runtime running | ||
@@ -518,3 +581,3 @@ // (and timing out) when: | ||
| // https://nodejs.org/api/http.html#http_event_timeout. | ||
| destroyRequest(req); | ||
| cleanupRequest(req); | ||
@@ -528,6 +591,20 @@ const message = | ||
| logger.warn(`[${requestId}] ${message}`); | ||
| handleCallback(new Error(message)); | ||
| if (options.retries === false || tries >= 1) { | ||
| logger.debug(`[${requestId}] Giving up...`); | ||
| return handleCallback(); | ||
| } | ||
| logger.debug(`[${requestId}] Retrying...`); | ||
| send({ resourcePath, payload, finalLambdaRequest, callback: handleCallback, tries: tries + 1, requestId }); | ||
| } | ||
| } | ||
| function cleanupRequests() { | ||
| Object.keys(requests).forEach(key => { | ||
| const requestToCleanup = requests[key]; | ||
| cleanupRequest(requestToCleanup); | ||
| }); | ||
| } | ||
| function cleanupRequest(req) { | ||
@@ -534,0 +611,0 @@ // When the Node.js process is frozen while the request is pending, and then thawed later, |
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 3 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 7 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
66345
3.91%873
6.72%31
-3.12%+ Added
- Removed
Updated