@instana/serverless
Advanced tools
+169
| /* | ||
| * (c) Copyright IBM Corp. 2021 | ||
| * (c) Copyright Instana Inc. and contributors 2019 | ||
| */ | ||
| /* eslint-disable no-console */ | ||
| 'use strict'; | ||
| // 30 = info | ||
| let minLevel = 30; | ||
| const DEBUG_LEVEL = 20; | ||
| const consoleLogger = { | ||
| debug: createLogFn(20, console.debug || console.log), | ||
| info: createLogFn(30, console.log), | ||
| warn: createLogFn(40, console.warn), | ||
| error: createLogFn(50, console.error) | ||
| }; | ||
| let instanaServerlessLogger; | ||
| function createLogFn(level, fn) { | ||
| return function log() { | ||
| if (level >= minLevel) { | ||
| fn.apply(console, arguments); | ||
| } | ||
| }; | ||
| } | ||
| class InstanaServerlessLogger { | ||
| /** | ||
| * @param {import('@instana/core/src/core').GenericLogger} logger | ||
| */ | ||
| constructor(logger) { | ||
| this.logger = logger; | ||
| } | ||
| /** | ||
| * @param {import('@instana/core/src/core').GenericLogger} _logger | ||
| */ | ||
| setLogger(_logger) { | ||
| this.logger = _logger; | ||
| } | ||
| isInDebugMode() { | ||
| if (!this.logger) return false; | ||
| if (typeof this.logger.level === 'function') { | ||
| return this.logger.level() === DEBUG_LEVEL; | ||
| } | ||
| if (typeof this.logger.level === 'number') { | ||
| return this.logger.level === DEBUG_LEVEL; | ||
| } | ||
| if (typeof this.logger.getLevel === 'function') { | ||
| return this.logger.getLevel() === DEBUG_LEVEL; | ||
| } | ||
| return minLevel === DEBUG_LEVEL; | ||
| } | ||
| warn = (...args) => this.logger?.warn?.(...args); | ||
| error = (...args) => this.logger?.error?.(...args); | ||
| info = (...args) => this.logger?.info?.(...args); | ||
| debug = (...args) => this.logger?.debug?.(...args); | ||
| trace = (...args) => this.logger?.trace?.(...args); | ||
| } | ||
| exports.init = function init(userConfig = {}) { | ||
| let parentLogger; | ||
| // CASE: prevent circular references | ||
| if (userConfig.logger && userConfig.logger instanceof InstanaServerlessLogger && userConfig.logger.logger) { | ||
| userConfig.logger = userConfig.logger.logger; | ||
| } | ||
| // CASE: customer overrides logger in serverless land. | ||
| if (userConfig.logger && typeof userConfig.logger.child === 'function') { | ||
| // A bunyan or pino logger has been provided via config. In either case we create a child logger directly under the | ||
| // given logger which serves as the parent for all loggers we create later on. | ||
| // BUG: Winston does not support child logger levels! Neither in `.child` nor with `level()` | ||
| // Setting INSTANA_DEBUG=true has no affect in the child winston logger. | ||
| // It takes the parent logger level. | ||
| parentLogger = userConfig.logger.child({ | ||
| module: 'instana-nodejs-serverless-logger' | ||
| }); | ||
| } else if (userConfig.logger && hasLoggingFunctions(userConfig.logger)) { | ||
| // A custom non-bunyan/non-pino logger has been provided via config. We use it as is. | ||
| parentLogger = userConfig.logger; | ||
| } else { | ||
| parentLogger = consoleLogger; | ||
| } | ||
| // NOTE: We accept for `process.env.INSTANA_DEBUG` any string value - does not have to be "true". | ||
| if (process.env.INSTANA_DEBUG || userConfig.level || process.env.INSTANA_LOG_LEVEL) { | ||
| setLoggerLevel(process.env.INSTANA_DEBUG ? 'debug' : userConfig.level || process.env.INSTANA_LOG_LEVEL); | ||
| } | ||
| if (!instanaServerlessLogger) { | ||
| instanaServerlessLogger = new InstanaServerlessLogger(parentLogger); | ||
| } else { | ||
| instanaServerlessLogger.setLogger(parentLogger); | ||
| } | ||
| return instanaServerlessLogger; | ||
| }; | ||
| exports.getLogger = () => { | ||
| return instanaServerlessLogger; | ||
| }; | ||
| // TODO: Legacy. Remove in next major release. | ||
| ['info', 'warn', 'error', 'debug'].forEach(level => { | ||
| exports[level] = function () { | ||
| if (!instanaServerlessLogger) { | ||
| exports.init(); | ||
| } | ||
| return instanaServerlessLogger[level].apply(instanaServerlessLogger, arguments); | ||
| }; | ||
| }); | ||
| /** | ||
| * @param {import('@instana/core/src/core').GenericLogger | *} _logger | ||
| * @returns {boolean} | ||
| */ | ||
| function hasLoggingFunctions(_logger) { | ||
| return ( | ||
| typeof _logger.debug === 'function' && | ||
| typeof _logger.info === 'function' && | ||
| typeof _logger.warn === 'function' && | ||
| typeof _logger.error === 'function' | ||
| ); | ||
| } | ||
| function setLoggerLevel(level) { | ||
| // eslint-disable-next-line yoda | ||
| if (typeof level === 'number' && 0 < level && level <= 50) { | ||
| minLevel = level; | ||
| return; | ||
| } | ||
| if (typeof level === 'string') { | ||
| switch (level) { | ||
| case 'debug': | ||
| minLevel = 20; | ||
| break; | ||
| case 'info': | ||
| minLevel = 30; | ||
| break; | ||
| case 'warn': | ||
| minLevel = 40; | ||
| break; | ||
| case 'error': | ||
| minLevel = 50; | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| } |
+392
-8
@@ -6,20 +6,404 @@ # Change Log | ||
| # [5.0.0](https://github.com/instana/nodejs/compare/v3.21.0...v5.0.0) (2024-10-23) | ||
| ## [5.0.1](https://github.com/instana/nodejs/compare/v5.0.0...v5.0.1) (2025-12-16) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [5.0.0](https://github.com/instana/nodejs/compare/v4.31.0...v5.0.0) (2025-12-16) | ||
| ### Bug Fixes | ||
| - dropped support for node v14 and v16 ([#1348](https://github.com/instana/nodejs/issues/1348)) ([aaa9ad4](https://github.com/instana/nodejs/commit/aaa9ad41ebf82b11eedcf913afc31d3addd53868)) | ||
| - removed deprecated INSTANA_URL and INSTANA_KEY environment variables ([#1373](https://github.com/instana/nodejs/issues/1373)) ([955cf67](https://github.com/instana/nodejs/commit/955cf67f4c83757329a8a1ad9b843dc8801b4300)) | ||
| * enforced Node.js 18.19 as the minimum supported version ([#2151](https://github.com/instana/nodejs/issues/2151)) ([5d688e2](https://github.com/instana/nodejs/commit/5d688e22ff03a6e4721a0363011002801a1ee045)) | ||
| * removed setLogger fn from serverless logger ([#2188](https://github.com/instana/nodejs/issues/2188)) ([47763f5](https://github.com/instana/nodejs/commit/47763f5d683d191c5512a804359adfb7e7f925de)) | ||
| ### BREAKING CHANGES | ||
| - - The INSTANA_URL and INSTANA_KEY environment variables have been removed. | ||
| * Dropped support for Node.js versions below 18.19 | ||
| * Any references to these should be replaced with the environment variables INSTANA_ENDPOINT_URL and INSTANA_AGENT_KEY. | ||
| - - Dropped support for Node.js versions 14 and 16. | ||
| * Reason: These versions have reached their end of life. | ||
| * More info: https://github.com/nodejs/Release?tab=readme-ov-file#end-of-life-releases | ||
| # [4.31.0](https://github.com/instana/nodejs/compare/v4.30.1...v4.31.0) (2025-12-08) | ||
| ### Bug Fixes | ||
| * **serverless:** resolved TypeError when agent key is not available ([#2197](https://github.com/instana/nodejs/issues/2197)) ([d24e759](https://github.com/instana/nodejs/commit/d24e759cd220dfb92975d88fd845cd3de5b99ad2)) | ||
| ## [4.30.1](https://github.com/instana/nodejs/compare/v4.30.0...v4.30.1) (2025-11-18) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.30.0](https://github.com/instana/nodejs/compare/v4.29.0...v4.30.0) (2025-11-17) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.29.0](https://github.com/instana/nodejs/compare/v4.28.0...v4.29.0) (2025-11-07) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.28.0](https://github.com/instana/nodejs/compare/v4.27.1...v4.28.0) (2025-11-06) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.27.1](https://github.com/instana/nodejs/compare/v4.27.0...v4.27.1) (2025-11-03) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.27.0](https://github.com/instana/nodejs/compare/v4.26.4...v4.27.0) (2025-10-23) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.26.4](https://github.com/instana/nodejs/compare/v4.26.3...v4.26.4) (2025-10-21) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.26.3](https://github.com/instana/nodejs/compare/v4.26.2...v4.26.3) (2025-10-21) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.26.2](https://github.com/instana/nodejs/compare/v4.26.1...v4.26.2) (2025-10-15) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.26.1](https://github.com/instana/nodejs/compare/v4.26.0...v4.26.1) (2025-10-13) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.26.0](https://github.com/instana/nodejs/compare/v4.25.0...v4.26.0) (2025-10-08) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.25.0](https://github.com/instana/nodejs/compare/v4.24.1...v4.25.0) (2025-09-23) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.24.1](https://github.com/instana/nodejs/compare/v4.24.0...v4.24.1) (2025-09-18) | ||
| ### Bug Fixes | ||
| * forced debug dependency to ^4.4.3 ([#2007](https://github.com/instana/nodejs/issues/2007)) ([3d5caaa](https://github.com/instana/nodejs/commit/3d5caaa70a9b841945a4adfe5978db54734cf9fa)) | ||
| # [4.24.0](https://github.com/instana/nodejs/compare/v4.23.1...v4.24.0) (2025-09-11) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.23.1](https://github.com/instana/nodejs/compare/v4.23.0...v4.23.1) (2025-09-01) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.23.0](https://github.com/instana/nodejs/compare/v4.22.0...v4.23.0) (2025-08-25) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.22.0](https://github.com/instana/nodejs/compare/v4.21.3...v4.22.0) (2025-08-13) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.21.3](https://github.com/instana/nodejs/compare/v4.21.2...v4.21.3) (2025-08-07) | ||
| ### Bug Fixes | ||
| * **aws-lambda:** improved coldstarts ([#1933](https://github.com/instana/nodejs/issues/1933)) ([cfede29](https://github.com/instana/nodejs/commit/cfede297893f4535b6a53602a8737c07cb4c7f5a)) | ||
| * bumped https-proxy-agent from 7.0.2 to 7.0.6 ([#1929](https://github.com/instana/nodejs/issues/1929)) ([e8f1c4d](https://github.com/instana/nodejs/commit/e8f1c4d356d35880fa38876d7502df5c6e15784f)) | ||
| ## [4.21.2](https://github.com/instana/nodejs/compare/v4.21.1...v4.21.2) (2025-08-05) | ||
| ### Bug Fixes | ||
| * **aws-lambda:** improved heartbeat timeout ([#1912](https://github.com/instana/nodejs/issues/1912)) ([08290bb](https://github.com/instana/nodejs/commit/08290bbe83772c3c49bb4cb4cdab0a745c7b6455)) | ||
| ## [4.21.1](https://github.com/instana/nodejs/compare/v4.21.0...v4.21.1) (2025-08-05) | ||
| ### Bug Fixes | ||
| * **aws-lambda:** increased initial heartbeat timeout ([6b1b1df](https://github.com/instana/nodejs/commit/6b1b1df37cfd04ab051f0472c3ea29ed81d852fa)) | ||
| * **aws-lambda:** transformed heartbeat warning into debug log ([a6bf4dd](https://github.com/instana/nodejs/commit/a6bf4dd09ab2ea3a79e2d221eac67412968e7f89)) | ||
| # [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) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.19.1](https://github.com/instana/nodejs/compare/v4.19.0...v4.19.1) (2025-07-25) | ||
| ### Bug Fixes | ||
| * **serverless:** resolved maximum call stack error ([#1877](https://github.com/instana/nodejs/issues/1877)) ([985b3c1](https://github.com/instana/nodejs/commit/985b3c165b0533a068e9e37c1e0b99a1fbfb4da0)) | ||
| # [4.19.0](https://github.com/instana/nodejs/compare/v4.18.1...v4.19.0) (2025-07-24) | ||
| ### Bug Fixes | ||
| * **serverless:** removed trailing slashes from instana endpoint url ([#1862](https://github.com/instana/nodejs/issues/1862)) ([305f2b2](https://github.com/instana/nodejs/commit/305f2b249900d6e5e0ffb7b305b486fd1cf2621a)) | ||
| ## [4.18.1](https://github.com/instana/nodejs/compare/v4.18.0...v4.18.1) (2025-07-14) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.18.0](https://github.com/instana/nodejs/compare/v4.17.0...v4.18.0) (2025-07-10) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.17.0](https://github.com/instana/nodejs/compare/v4.16.0...v4.17.0) (2025-06-30) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.16.0](https://github.com/instana/nodejs/compare/v4.15.3...v4.16.0) (2025-06-24) | ||
| ### Bug Fixes | ||
| * **collector:** optimized flushing spans before application dies ([#1765](https://github.com/instana/nodejs/issues/1765)) ([3507599](https://github.com/instana/nodejs/commit/35075996e879734a7bb9437c1ce375b9d979fe3a)), closes [#1315](https://github.com/instana/nodejs/issues/1315) | ||
| ## [4.15.3](https://github.com/instana/nodejs/compare/v4.15.2...v4.15.3) (2025-06-11) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.15.2](https://github.com/instana/nodejs/compare/v4.15.1...v4.15.2) (2025-06-11) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.15.1](https://github.com/instana/nodejs/compare/v4.15.0...v4.15.1) (2025-06-09) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.15.0](https://github.com/instana/nodejs/compare/v4.14.0...v4.15.0) (2025-05-27) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.14.0](https://github.com/instana/nodejs/compare/v4.13.0...v4.14.0) (2025-05-13) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.13.0](https://github.com/instana/nodejs/compare/v4.12.0...v4.13.0) (2025-05-08) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.12.0](https://github.com/instana/nodejs/compare/v4.11.1...v4.12.0) (2025-05-06) | ||
| ### Bug Fixes | ||
| - **aws-lambda:** resolved TypeError when memory is too low ([#1715](https://github.com/instana/nodejs/issues/1715)) ([03a8eb3](https://github.com/instana/nodejs/commit/03a8eb39f386af44f98c5405d668369b3736bff0)) | ||
| ## [4.11.1](https://github.com/instana/nodejs/compare/v4.11.0...v4.11.1) (2025-04-24) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.11.0](https://github.com/instana/nodejs/compare/v4.10.0...v4.11.0) (2025-04-22) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.10.0](https://github.com/instana/nodejs/compare/v4.9.0...v4.10.0) (2025-04-01) | ||
| ### Features | ||
| - added support for express v5 ([#1654](https://github.com/instana/nodejs/issues/1654)) ([8576291](https://github.com/instana/nodejs/commit/85762916de721833f6722fbb75f978aceafd83a8)) | ||
| # [4.9.0](https://github.com/instana/nodejs/compare/v4.8.0...v4.9.0) (2025-03-20) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.8.0](https://github.com/instana/nodejs/compare/v4.7.0...v4.8.0) (2025-03-19) | ||
| ### Features | ||
| - **serverless:** added request id to instana debug logs ([#1623](https://github.com/instana/nodejs/issues/1623)) ([c5a1b69](https://github.com/instana/nodejs/commit/c5a1b691ed9f01363bbc76bc262f985a4901744e)) | ||
| # [4.7.0](https://github.com/instana/nodejs/compare/v4.6.3...v4.7.0) (2025-03-11) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.6.3](https://github.com/instana/nodejs/compare/v4.6.2...v4.6.3) (2025-03-05) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.6.2](https://github.com/instana/nodejs/compare/v4.6.1...v4.6.2) (2025-02-24) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.6.1](https://github.com/instana/nodejs/compare/v4.6.0...v4.6.1) (2025-01-29) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.6.0](https://github.com/instana/nodejs/compare/v4.5.3...v4.6.0) (2025-01-18) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.5.3](https://github.com/instana/nodejs/compare/v4.5.2...v4.5.3) (2025-01-14) | ||
| ### Bug Fixes | ||
| - resolved more logging objects structure ([#1510](https://github.com/instana/nodejs/issues/1510)) ([bd4c9bb](https://github.com/instana/nodejs/commit/bd4c9bbda2c82aee7f6c59fcca03ac5588566839)) | ||
| ## [4.5.2](https://github.com/instana/nodejs/compare/v4.5.1...v4.5.2) (2025-01-13) | ||
| ### Bug Fixes | ||
| - resolved logging objects being undefined or missing ([#1509](https://github.com/instana/nodejs/issues/1509)) ([7715fed](https://github.com/instana/nodejs/commit/7715fed5843716a6e49d79f221efcec33a9a1c9d)) | ||
| ## [4.5.1](https://github.com/instana/nodejs/compare/v4.5.0...v4.5.1) (2025-01-13) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.5.0](https://github.com/instana/nodejs/compare/v4.4.0...v4.5.0) (2024-12-16) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.4.0](https://github.com/instana/nodejs/compare/v4.3.0...v4.4.0) (2024-12-12) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.3.0](https://github.com/instana/nodejs/compare/v4.2.0...v4.3.0) (2024-12-10) | ||
| ### Features | ||
| - **serverless-collector:** added service name detection ([#1468](https://github.com/instana/nodejs/issues/1468)) ([5491461](https://github.com/instana/nodejs/commit/54914613bf40c6e81c652801899f2d3136308073)) | ||
| # [4.2.0](https://github.com/instana/nodejs/compare/v4.1.0...v4.2.0) (2024-11-22) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.1.0](https://github.com/instana/nodejs/compare/v4.0.1...v4.1.0) (2024-11-19) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| ## [4.0.1](https://github.com/instana/nodejs/compare/v4.0.0...v4.0.1) (2024-10-28) | ||
| **Note:** Version bump only for package @instana/serverless | ||
| # [4.0.0](https://github.com/instana/nodejs/compare/v3.21.0...v4.0.0) (2024-10-23) | ||
@@ -26,0 +410,0 @@ |
+15
-10
| { | ||
| "name": "@instana/serverless", | ||
| "version": "5.0.0", | ||
| "version": "5.0.1", | ||
| "description": "Internal utility package for serverless Node.js tracing and monitoring with Instana", | ||
@@ -15,2 +15,5 @@ "author": { | ||
| ], | ||
| "engines": { | ||
| "node": ">=18.19.0" | ||
| }, | ||
| "publishConfig": { | ||
@@ -32,3 +35,3 @@ "access": "public" | ||
| "test:ci": "mocha --config=test/.mocharc.js --reporter mocha-multi-reporters --reporter-options configFile=reporter-config.json 'test/**/*test.js'", | ||
| "test:debug": "WITH_STDOUT=true npm run test", | ||
| "test:debug": "WITH_STDOUT=true npm run test --", | ||
| "lint": "eslint src test", | ||
@@ -53,6 +56,2 @@ "verify": "npm run lint && npm test", | ||
| { | ||
| "name": "Willian Carvalho", | ||
| "email": "willian.carvalho@instana.com" | ||
| }, | ||
| { | ||
| "name": "Katharina Irrgang", | ||
@@ -64,2 +63,6 @@ "email": "katharina.irrgang@ibm.com" | ||
| "email": "arya.mohanan@ibm.com" | ||
| }, | ||
| { | ||
| "name": "Abhilash Sivan", | ||
| "email": "abhilash.sivan@ibm.com" | ||
| } | ||
@@ -72,7 +75,9 @@ ], | ||
| "dependencies": { | ||
| "@instana/core": "5.0.0", | ||
| "agent-base": "^6.0.2", | ||
| "https-proxy-agent": "^7.0.2" | ||
| "@instana/core": "5.0.1", | ||
| "https-proxy-agent": "^7.0.6" | ||
| }, | ||
| "gitHead": "209e1ac4c64c5224c99bffcc82a172c3d22e4438" | ||
| "overrides": { | ||
| "debug": "^4.4.3" | ||
| }, | ||
| "gitHead": "77fa60d1e6d88a852540f0e88ff3a8b2e36826ac" | ||
| } |
+345
-238
@@ -11,6 +11,3 @@ /* | ||
| const uninstrumented = require('./uninstrumentedHttp'); | ||
| const constants = require('./constants'); | ||
| let logger = require('./console_logger'); | ||
| const layerExtensionHostname = 'localhost'; | ||
@@ -20,67 +17,104 @@ const layerExtensionPort = process.env.INSTANA_LAYER_EXTENSION_PORT | ||
| : 7365; | ||
| let useLambdaExtension = false; | ||
| const timeoutEnvVar = 'INSTANA_TIMEOUT'; | ||
| let defaultTimeout = 500; | ||
| const layerExtensionTimeout = process.env.INSTANA_LAMBDA_EXTENSION_TIMEOUT_IN_MS | ||
| // NOTE: The heartbeat is usually really, really fast (<30ms). | ||
| // But we have seen in some cases that it can take up to 500ms. | ||
| const layerExtensionHeartbeatTimeout = 500; | ||
| // NOTE: The initial heartbeat can be very slow when the Lambda is in cold start. | ||
| const initialLayerExtensionHeartbeatTimeout = 3000; | ||
| // 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 = 3000; | ||
| const layerExtensionRequestTimeout = process.env.INSTANA_LAMBDA_EXTENSION_TIMEOUT_IN_MS | ||
| ? Number(process.env.INSTANA_LAMBDA_EXTENSION_TIMEOUT_IN_MS) | ||
| : 500; | ||
| let backendTimeout = defaultTimeout; | ||
| const proxyEnvVar = 'INSTANA_ENDPOINT_PROXY'; | ||
| const disableCaCheckEnvVar = 'INSTANA_DISABLE_CA_CHECK'; | ||
| const disableCaCheck = process.env[disableCaCheckEnvVar] === 'true'; | ||
| let proxyAgent; | ||
| let stopSendingOnFailure = true; | ||
| let propagateErrorsUpstream = false; | ||
| let requestHasFailed = false; | ||
| let warningsHaveBeenLogged = false; | ||
| let firstRequestToExtension = true; | ||
| const disableCaCheckEnvVar = 'INSTANA_DISABLE_CA_CHECK'; | ||
| const disableCaCheck = process.env[disableCaCheckEnvVar] === 'true'; | ||
| const defaults = { | ||
| config: {}, | ||
| identityProvider: null, | ||
| isLambdaRequest: false, | ||
| backendTimeout: 500, | ||
| useLambdaExtension: false, | ||
| retries: false | ||
| }; | ||
| if (process.env[proxyEnvVar] && !environmentUtil.sendUnencrypted) { | ||
| const proxyUrl = process.env[proxyEnvVar]; | ||
| logger.info( | ||
| `The environment variable ${proxyEnvVar} is set. Requests to the Instana back end will be routed via a proxy ` + | ||
| `server: ${proxyUrl}.` | ||
| ); | ||
| let logger; | ||
| let options; | ||
| let hostHeader; | ||
| const { HttpsProxyAgent } = require('https-proxy-agent'); | ||
| proxyAgent = new HttpsProxyAgent(proxyUrl); | ||
| } else if (process.env[proxyEnvVar] && environmentUtil.sendUnencrypted) { | ||
| logger.warn( | ||
| `Both ${proxyEnvVar} and ${environmentUtil.sendUnencryptedEnvVar} are set, but this combination is not supported.` + | ||
| ' Requests to the Instana back end will not be routed via a proxy server.' | ||
| ); | ||
| } | ||
| /** | ||
| * Most of the logs are debug logs in the backend connector, because | ||
| * on serverless we do not want to log too much. | ||
| * If the debug mode is enabled, we want to add a request id to the instana | ||
| * logs because the AWS runtime freezes requests and they are waking up | ||
| * as soon as the next request is coming in. With the request id we can | ||
| * identify the logs of a single request. | ||
| * | ||
| * Due to performance reasons, we do not want to generate a request id | ||
| * in the non debug mode. | ||
| */ | ||
| const getRequestId = () => { | ||
| if (logger && logger.isInDebugMode && logger.isInDebugMode()) { | ||
| // Although the usage of "Math.random()"" is not allowed for being FedRamp compliant, but | ||
| // this use case is a non secure workflow. | ||
| return `instana_${Date.now().toString(36) + Math.random().toString(36).slice(2)}`; | ||
| } | ||
| let hostHeader; | ||
| return 'instana'; | ||
| }; | ||
| exports.init = function init( | ||
| identityProvider, | ||
| _logger, | ||
| _stopSendingOnFailure, | ||
| _propagateErrorsUpstream, | ||
| _defaultTimeout, | ||
| _useLambdaExtension | ||
| ) { | ||
| stopSendingOnFailure = _stopSendingOnFailure == null ? true : _stopSendingOnFailure; | ||
| propagateErrorsUpstream = _propagateErrorsUpstream == null ? false : _propagateErrorsUpstream; | ||
| defaultTimeout = _defaultTimeout == null ? defaultTimeout : _defaultTimeout; | ||
| useLambdaExtension = _useLambdaExtension; | ||
| backendTimeout = defaultTimeout; | ||
| const requests = {}; | ||
| exports.init = function init(opts) { | ||
| options = Object.assign(defaults, opts); | ||
| logger = options.config.logger; | ||
| // TODO: refactor environment.js into serverless normalize config | ||
| // and move the following code into the new unit | ||
| if (process.env[proxyEnvVar] && !environmentUtil.sendUnencrypted) { | ||
| const proxyUrl = process.env[proxyEnvVar]; | ||
| logger.info( | ||
| `The environment variable ${proxyEnvVar} is set. Requests to the Instana back end will be routed via a proxy ` + | ||
| `server: ${proxyUrl}.` | ||
| ); | ||
| const { HttpsProxyAgent } = require('https-proxy-agent'); | ||
| proxyAgent = new HttpsProxyAgent(proxyUrl); | ||
| } else if (process.env[proxyEnvVar] && environmentUtil.sendUnencrypted) { | ||
| logger.warn( | ||
| `Both ${proxyEnvVar} and ${environmentUtil.sendUnencryptedEnvVar} are set, ` + | ||
| 'but this combination is not supported.' + | ||
| ' Requests to the Instana back end will not be routed via a proxy server.' | ||
| ); | ||
| } | ||
| // TODO: refactor environment.js into serverless normalize config | ||
| // and move the following code into the new unit | ||
| if (process.env[timeoutEnvVar]) { | ||
| backendTimeout = parseInt(process.env[timeoutEnvVar], 10); | ||
| if (isNaN(backendTimeout) || backendTimeout < 0) { | ||
| options.backendTimeout = parseInt(process.env[timeoutEnvVar], 10); | ||
| if (isNaN(options.backendTimeout) || options.backendTimeout < 0) { | ||
| logger.warn( | ||
| `The value of ${timeoutEnvVar} (${process.env[timeoutEnvVar]}) cannot be parsed to a valid numerical value. ` + | ||
| `Will fall back to the default timeout (${defaultTimeout} ms).` | ||
| `Will fall back to the default timeout (${defaults.backendTimeout} ms).` | ||
| ); | ||
| backendTimeout = defaultTimeout; | ||
| options.backendTimeout = defaults.backendTimeout; | ||
| } | ||
| } | ||
| if (identityProvider) { | ||
| hostHeader = identityProvider.getHostHeader(); | ||
| if (options.identityProvider) { | ||
| hostHeader = options.identityProvider.getHostHeader(); | ||
| if (hostHeader == null) { | ||
@@ -93,8 +127,2 @@ hostHeader = 'nodejs-serverless'; | ||
| if (_logger) { | ||
| logger = _logger; | ||
| } | ||
| requestHasFailed = false; | ||
| // Heartbeat is only for the AWS Lambda extension | ||
@@ -104,3 +132,3 @@ // IMPORTANT: the @instana/aws-lambda package will not | ||
| // SpanBuffer sends data asap and when the handler is finished the rest is sent. | ||
| if (useLambdaExtension) { | ||
| if (options.useLambdaExtension) { | ||
| scheduleLambdaExtensionHeartbeatRequest(); | ||
@@ -110,36 +138,40 @@ } | ||
| 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`. | ||
| */ | ||
| exports.sendBundle = function sendBundle(bundle, finalLambdaRequest, callback) { | ||
| logger.debug(`Sending bundle to Instana (no. of spans: ${bundle?.spans?.length ?? 'unknown'})`); | ||
| send('/bundle', bundle, finalLambdaRequest, callback); | ||
| const requestId = getRequestId(); | ||
| logger.debug(`[${requestId}] Sending bundle to Instana (no. of spans: ${bundle?.spans?.length ?? 'unknown'})`); | ||
| send({ resourcePath: '/bundle', payload: bundle, finalLambdaRequest, callback, requestId }); | ||
| }; | ||
| exports.sendMetrics = function sendMetrics(metrics, callback) { | ||
| send('/metrics', metrics, false, callback); | ||
| const requestId = getRequestId(); | ||
| logger.debug(`[${requestId}] Sending metrics to Instana (no. of metrics: ${metrics?.plugins?.length})`); | ||
| send({ resourcePath: '/metrics', payload: metrics, finalLambdaRequest: false, callback, requestId }); | ||
| }; | ||
| exports.sendSpans = function sendSpans(spans, callback) { | ||
| logger.debug(`Sending spans to Instana (no. of spans: ${spans.length})`); | ||
| send('/traces', spans, false, callback); | ||
| const requestId = getRequestId(); | ||
| logger.debug(`[${requestId}] Sending spans to Instana (no. of spans: ${spans?.length})`); | ||
| send({ resourcePath: '/traces', payload: spans, finalLambdaRequest: false, callback, requestId }); | ||
| }; | ||
| let heartbeatInterval; | ||
| let heartbeatIsActive = false; | ||
| function scheduleLambdaExtensionHeartbeatRequest() { | ||
| const executeHeartbeat = () => { | ||
| logger.debug('Executing Heartbeat request to Lambda extension.'); | ||
| const executeHeartbeat = (heartbeatOpts = {}) => { | ||
| if (heartbeatIsActive) { | ||
| return; | ||
| } | ||
| const startTime = Date.now(); | ||
| const requestId = getRequestId(); | ||
| logger.debug(`[${requestId}] Executing Heartbeat request to Lambda extension.`); | ||
| heartbeatIsActive = true; | ||
| const req = uninstrumented.http.request( | ||
@@ -151,16 +183,17 @@ { | ||
| 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('The Instana Lambda extension Heartbeat request has succeeded.'); | ||
| logger.debug(`[${requestId}] The Instana Lambda extension heartbeat request has succeeded.`); | ||
| } else { | ||
| handleHeartbeatError( | ||
| new Error( | ||
| `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(); | ||
| } | ||
@@ -170,2 +203,3 @@ | ||
| // we need to register the handlers to avoid running into a timeout | ||
| // because the request expects to receive body data | ||
| }); | ||
@@ -176,3 +210,5 @@ | ||
| const duration = endTime - startTime; | ||
| logger.debug(`Took ${duration}ms to receive response from extension`); | ||
| logger.debug(`[${requestId}] Took ${duration}ms to receive response from extension for the heartbeat.`); | ||
| heartbeatIsActive = false; | ||
| }); | ||
@@ -182,21 +218,17 @@ } | ||
| req.once('finish', () => { | ||
| const endTime = Date.now(); | ||
| const duration = endTime - startTime; | ||
| logger.debug(`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. | ||
| useLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| req.setTimeout(heartbeatOpts.heartbeatTimeout, () => { | ||
| logger.debug(`[${requestId}] Heartbeat request timed out.`); | ||
| logger.debug( | ||
| 'The Instana Lambda extension Heartbeat request did not succeed. Falling back to talking to the Instana back ' + | ||
| 'end directly.', | ||
| e | ||
| ); | ||
| } | ||
| req.once('error', e => { | ||
| // req.destroyed indicates that we have run into a timeout and have already handled the timeout error. | ||
@@ -207,18 +239,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('The Lambda extension Heartbeat request timed out.')); | ||
| function handleHeartbeatError() { | ||
| logger.debug(`[${requestId}] Heartbeat request failed. Falling back to the serverless acceptor instead.`); | ||
| // Destroy timed out request manually as mandated in https://nodejs.org/api/http.html#event-timeout. | ||
| if (req && !req.destroyed) { | ||
| try { | ||
| destroyRequest(req); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| } | ||
| }); | ||
| options.useLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| cleanupRequest(req); | ||
| heartbeatIsActive = false; | ||
| } | ||
@@ -229,11 +256,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. | ||
@@ -246,7 +277,16 @@ return uninstrumented.http; | ||
| function getBackendTimeout(localUseLambdaExtension) { | ||
| return localUseLambdaExtension ? layerExtensionTimeout : backendTimeout; | ||
| function getBackendTimeout() { | ||
| if (options.useLambdaExtension) { | ||
| if (firstRequestToExtension) { | ||
| firstRequestToExtension = false; | ||
| return initialLayerExtensionRequestTimeout; | ||
| } else { | ||
| return layerExtensionRequestTimeout; | ||
| } | ||
| } | ||
| return options.backendTimeout; | ||
| } | ||
| function send(resourcePath, payload, finalLambdaRequest, callback) { | ||
| function send({ resourcePath, payload, finalLambdaRequest, callback, tries, requestId }) { | ||
| let callbackWasCalled = false; | ||
@@ -259,14 +299,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 = useLambdaExtension; | ||
| if (requestHasFailed && stopSendingOnFailure) { | ||
| logger.info( | ||
| `Not attempting to send data to ${resourcePath} as a previous request has already timed out or failed.` | ||
| ); | ||
| handleCallback(); | ||
| return; | ||
| if (tries === undefined) { | ||
| tries = 0; | ||
| } | ||
@@ -277,4 +307,5 @@ | ||
| if (environmentUtil.sendUnencrypted) { | ||
| logger.error( | ||
| `${environmentUtil.sendUnencryptedEnvVar} is set, which means that all traffic to Instana is send ` + | ||
| logger.warn( | ||
| `[${requestId}] ${environmentUtil.sendUnencryptedEnvVar} is set, which means that all traffic ` + | ||
| 'to Instana is send ' + | ||
| 'unencrypted via plain HTTP, not via HTTPS. This will effectively make that traffic public. This setting ' + | ||
@@ -284,5 +315,7 @@ 'should never be used in production.' | ||
| } | ||
| if (disableCaCheck) { | ||
| logger.warn( | ||
| `${disableCaCheckEnvVar} is set, which means that the server certificate will not be verified against ` + | ||
| `[${requestId}] ${disableCaCheckEnvVar} is set, which means that the server certificate will ` + | ||
| 'not be verified against ' + | ||
| 'the list of known CAs. This makes your service vulnerable to MITM attacks when connecting to Instana. ' + | ||
@@ -295,16 +328,23 @@ 'This setting should never be used in production, unless you use our on-premises product and are unable to ' + | ||
| const instanaAgentKey = environmentUtil.getInstanaAgentKey(); | ||
| // CASE: aws lambda ssm key cannot be fetched, but either detached requests are sent | ||
| // after the handler finished or coldstart delayed the handler execution | ||
| if (!instanaAgentKey) { | ||
| logger.warn(`[${requestId}] No Instana agent key configured. Cannot send data to Instana.`); | ||
| return handleCallback(); | ||
| } | ||
| // prepend backend's path if the configured URL has a path component | ||
| const requestPath = | ||
| localUseLambdaExtension || environmentUtil.getBackendPath() === '/' | ||
| options.useLambdaExtension || environmentUtil.getBackendPath() === '/' | ||
| ? resourcePath | ||
| : environmentUtil.getBackendPath() + resourcePath; | ||
| logger.debug(`Sending data to Instana (${requestPath}).`); | ||
| // serialize the payload object | ||
| const serializedPayload = JSON.stringify(payload); | ||
| const options = { | ||
| hostname: localUseLambdaExtension ? layerExtensionHostname : environmentUtil.getBackendHost(), | ||
| port: localUseLambdaExtension ? layerExtensionPort : environmentUtil.getBackendPort(), | ||
| const reqOptions = { | ||
| hostname: options.useLambdaExtension ? layerExtensionHostname : environmentUtil.getBackendHost(), | ||
| port: options.useLambdaExtension ? layerExtensionPort : environmentUtil.getBackendPort(), | ||
| path: requestPath, | ||
@@ -315,4 +355,5 @@ method: 'POST', | ||
| 'Content-Length': Buffer.byteLength(serializedPayload), | ||
| Connection: 'keep-alive', | ||
| [constants.xInstanaHost]: hostHeader, | ||
| [constants.xInstanaKey]: environmentUtil.getInstanaAgentKey() | ||
| [constants.xInstanaKey]: instanaAgentKey | ||
| }, | ||
@@ -322,11 +363,19 @@ rejectUnauthorized: !disableCaCheck | ||
| options.timeout = getBackendTimeout(localUseLambdaExtension); | ||
| logger.debug( | ||
| `[${requestId}] Sending data to Instana (${reqOptions.hostname}, ${reqOptions.port}, ${reqOptions.path}, | ||
| ${reqOptions.headers?.['Content-Length']}).` | ||
| ); | ||
| if (proxyAgent && !localUseLambdaExtension) { | ||
| options.agent = proxyAgent; | ||
| // 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 && !options.useLambdaExtension) { | ||
| reqOptions.agent = proxyAgent; | ||
| } | ||
| let req; | ||
| const skipWaitingForHttpResponse = !proxyAgent && !localUseLambdaExtension; | ||
| const transport = getTransport(localUseLambdaExtension); | ||
| const skipWaitingForHttpResponse = !proxyAgent && !options.useLambdaExtension; | ||
| const transport = getTransport(options.useLambdaExtension); | ||
| const start = Date.now(); | ||
@@ -342,3 +391,3 @@ if (skipWaitingForHttpResponse) { | ||
| req = transport.request(options); | ||
| req = transport.request(reqOptions); | ||
| } else { | ||
@@ -353,3 +402,3 @@ // If (a) our Lambda extension is available, or if (b) a user-provided proxy is in use, we do *not* apply the | ||
| // to end the processing. Otherwise, the callback is provided here to http.request(). | ||
| req = transport.request(options, () => { | ||
| req = transport.request(reqOptions, () => { | ||
| // When the Node.js process is frozen while the request is pending, and then thawed later, | ||
@@ -362,9 +411,3 @@ // this can trigger a stale, bogus timeout event (because from the perspective of the freshly thawed Node.js | ||
| if (finalLambdaRequest) { | ||
| req.removeAllListeners(); | ||
| req.on('error', () => {}); | ||
| // Finally, abort the request because from our end we are no longer interested in the response and we also do | ||
| // not want to let pending IO actions linger in the event loop. This will also call request.destoy and | ||
| // req.socket.destroy() internally. | ||
| destroyRequest(req); | ||
| cleanupRequests(); | ||
| } | ||
@@ -376,2 +419,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' | ||
@@ -383,7 +446,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) | ||
| ); | ||
| 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 | ||
@@ -399,3 +474,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 | ||
@@ -406,39 +481,51 @@ // because @instana/aws-lambda has been installed as a normal npm dependency instead of using Instana's | ||
| // target in place. | ||
| logger.debug( | ||
| 'Could not connect to the Instana Lambda extension. Falling back to talking to the Instana back end directly.', | ||
| e | ||
| ); | ||
| 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. | ||
| 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); | ||
| // 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 trace data to ${resourcePath}. Could not connect to the configured proxy ` + | ||
| `${process.env[proxyEnvVar]}.` + | ||
| `${e?.message} ${e?.stack}` | ||
| ); | ||
| } else { | ||
| logger.warn( | ||
| `[${requestId}] Could not send trace data to ${resourcePath}. ` + | ||
| `The Instana back end seems to be unavailable. ${e?.message} , ${e?.stack}` | ||
| ); | ||
| } | ||
| if (!propagateErrorsUpstream) { | ||
| if (proxyAgent) { | ||
| logger.warn( | ||
| 'Could not send traces and metrics to Instana. Could not connect to the configured proxy ' + | ||
| `${process.env[proxyEnvVar]}.`, | ||
| e | ||
| ); | ||
| } else { | ||
| logger.warn('Could not send traces and metrics to Instana. The Instana back end seems to be unavailable.', e); | ||
| } | ||
| if (options.retries === false || tries >= 1) { | ||
| logger.debug(`[${requestId}] Giving up...`); | ||
| return handleCallback(e); | ||
| } | ||
| handleCallback(propagateErrorsUpstream ? e : undefined); | ||
| 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', () => { | ||
| logger.debug(`Sent data to Instana (${requestPath}).`); | ||
| logger.debug( | ||
| // eslint-disable-next-line max-len | ||
| `[${requestId}] The data have been successfully sent to Instana.` | ||
| ); | ||
| if (useLambdaExtension && finalLambdaRequest) { | ||
| if (options.useLambdaExtension && finalLambdaRequest) { | ||
| clearInterval(heartbeatInterval); | ||
@@ -449,20 +536,7 @@ } | ||
| 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) { | ||
| // When the Node.js process is frozen while the request is pending, and then thawed later, | ||
| // this can trigger a stale, bogus timeout event (because from the perspective of the freshly thawed Node.js | ||
| // runtime, the request has been pending and inactive since a long time). To avoid that, we remove all listeners | ||
| // (including the timeout listener) on the request. Since the Lambda runtime will be frozen afterwards (or | ||
| // reused for a different, unrelated invocation), it is safe to assume that we are no longer interested in any | ||
| // events emitted by the request or the underlying socket. | ||
| req.removeAllListeners(); | ||
| // We need to have a listener for errors that ignores everything, otherwise aborting the request/socket will | ||
| // produce an "Unhandled 'error' event" | ||
| req.on('error', () => {}); | ||
| // Finally, abort the request because from our end we are no longer interested in the response and we also do | ||
| // not want to let pending IO actions linger in the event loop. This will also call request.destoy and | ||
| // req.socket.destroy() internally. | ||
| destroyRequest(req); | ||
| if (options.isLambdaRequest && finalLambdaRequest) { | ||
| cleanupRequests(); | ||
| } | ||
@@ -480,35 +554,32 @@ | ||
| function onTimeout(localUseLambdaExtension, req, resourcePath, payload, finalLambdaRequest, handleCallback) { | ||
| 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( | ||
| '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. | ||
| useLambdaExtension = localUseLambdaExtension = false; | ||
| clearInterval(heartbeatInterval); | ||
| cleanupRequest(req); | ||
| if (req && !req.destroyed) { | ||
| try { | ||
| destroyRequest(req); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| // 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 }); | ||
| } | ||
| // Retry the request immediately, this time sending it to serverless-acceptor directly. | ||
| send(resourcePath, payload, finalLambdaRequest, handleCallback); | ||
| 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 (and timing out) when | ||
| // We need to destroy the request manually, otherwise it keeps the runtime running | ||
| // (and timing out) when: | ||
| // (a) the wrapped Lambda handler uses the callback API, and | ||
@@ -518,24 +589,60 @@ // (b) context.callbackWaitsForEmptyEventLoop = false is not set. | ||
| // https://nodejs.org/api/http.html#http_event_timeout. | ||
| if (req && !req.destroyed) { | ||
| try { | ||
| destroyRequest(req); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| } | ||
| cleanupRequest(req); | ||
| const message = | ||
| 'Could not send traces and metrics to Instana. The Instana back end did not respond in the configured timeout ' + | ||
| `of ${backendTimeout} ms. The timeout can be configured by setting the environment variable ${timeoutEnvVar}.`; | ||
| `[${requestId}] Could not send data to ${resourcePath}. The Instana back end did not respond ` + | ||
| 'in the configured timeout ' + | ||
| `of ${options.backendTimeout} ms. The timeout can be configured by ` + | ||
| `setting the environment variable ${timeoutEnvVar}.`; | ||
| if (!propagateErrorsUpstream) { | ||
| logger.warn(message); | ||
| logger.warn(`[${requestId}] ${message}`); | ||
| if (options.retries === false || tries >= 1) { | ||
| logger.debug(`[${requestId}] Giving up...`); | ||
| return handleCallback(); | ||
| } | ||
| handleCallback(propagateErrorsUpstream ? new Error(message) : undefined); | ||
| 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) { | ||
| // When the Node.js process is frozen while the request is pending, and then thawed later, | ||
| // this can trigger a stale, bogus timeout event (because from the perspective of the freshly thawed Node.js | ||
| // runtime, the request has been pending and inactive since a long time). | ||
| // To avoid that, we remove all listeners | ||
| // (including the timeout listener) on the request. Since the Lambda runtime will be frozen afterwards (or | ||
| // reused for a different, unrelated invocation), it is safe to assume that | ||
| // we are no longer interested in any | ||
| // events emitted by the request or the underlying socket. | ||
| req.removeAllListeners(); | ||
| // We need to have a listener for errors that ignores everything, otherwise aborting the request/socket will | ||
| // produce an "Unhandled 'error' event" | ||
| req.once('error', () => {}); | ||
| // Finally, abort the request because from our end we are no longer interested in the response and we also do | ||
| // not want to let pending IO actions linger in the event loop. This will also call request.destroy and | ||
| // req.socket.destroy() internally. | ||
| destroyRequest(req); | ||
| } | ||
| function destroyRequest(req) { | ||
| req.destroy(); | ||
| // Destroy timed out request manually as mandated in | ||
| // https://nodejs.org/api/http.html#event-timeout. | ||
| if (req && !req.destroyed) { | ||
| try { | ||
| req.destroy(); | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| } | ||
| } |
+16
-3
@@ -8,4 +8,2 @@ /* | ||
| const logger = require('./console_logger'); | ||
| const instanaEndpointUrlEnvVar = 'INSTANA_ENDPOINT_URL'; | ||
@@ -15,2 +13,3 @@ const instanaAgentKeyEnvVar = 'INSTANA_AGENT_KEY'; | ||
| let logger; | ||
| let valid = false; | ||
@@ -30,2 +29,6 @@ let backendHost = null; | ||
| exports.init = config => { | ||
| logger = config.logger; | ||
| }; | ||
| exports.validate = function validate({ validateInstanaAgentKey } = {}) { | ||
@@ -89,3 +92,3 @@ _validate(process.env[instanaEndpointUrlEnvVar], process.env[instanaAgentKeyEnvVar], validateInstanaAgentKey); | ||
| backendPath = parsedUrl.pathname; | ||
| backendPath = sanitizePath(parsedUrl.pathname); | ||
@@ -184,1 +187,11 @@ instanaAgentKey = _instanaAgentKey; | ||
| } | ||
| // Removes trailing slashes from the path (except when it's just '/') | ||
| // Prevents double slashes when building backend URLs. | ||
| // Example: "https://example.instana.io/serverless/" + "/bundle" → "https://example.instana.io/serverless/bundle" | ||
| function sanitizePath(pathName) { | ||
| if (!pathName || pathName === '/') { | ||
| return pathName; | ||
| } | ||
| return pathName.replace(/\/+$/, ''); | ||
| } |
+2
-1
@@ -9,5 +9,6 @@ /* | ||
| exports.backendConnector = require('./backend_connector'); | ||
| exports.consoleLogger = require('./console_logger'); | ||
| // TODO: rename in major release to simply "exports.logger" | ||
| exports.consoleLogger = require('./logger'); | ||
| exports.constants = require('./constants'); | ||
| exports.environment = require('./environment'); | ||
| exports.headers = require('./headers'); |
| /* | ||
| * (c) Copyright IBM Corp. 2021 | ||
| * (c) Copyright Instana Inc. and contributors 2019 | ||
| */ | ||
| /* eslint-disable no-console */ | ||
| 'use strict'; | ||
| let minLevel = 30; | ||
| module.exports = exports = { | ||
| debug: createLogFn(20, console.debug || console.log), | ||
| info: createLogFn(30, console.log), | ||
| warn: createLogFn(40, console.warn), | ||
| error: createLogFn(50, console.error) | ||
| }; | ||
| function createLogFn(level, fn) { | ||
| return function log() { | ||
| if (level >= minLevel) { | ||
| fn.apply(console, arguments); | ||
| } | ||
| }; | ||
| } | ||
| exports.setLevel = function setLevel(level) { | ||
| // eslint-disable-next-line yoda | ||
| if (typeof level === 'number' && 0 < level && level <= 50) { | ||
| minLevel = level; | ||
| return; | ||
| } | ||
| if (typeof level === 'string') { | ||
| switch (level) { | ||
| case 'debug': | ||
| minLevel = 20; | ||
| break; | ||
| case 'info': | ||
| minLevel = 30; | ||
| break; | ||
| case 'warn': | ||
| minLevel = 40; | ||
| break; | ||
| case 'error': | ||
| minLevel = 50; | ||
| break; | ||
| default: | ||
| exports.warn(`Unknown log level: ${level}`); | ||
| } | ||
| } | ||
| }; |
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 5 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
71823
34.78%2
-33.33%879
27.02%31
29.17%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated