diff --git a/bin/create-preinstalled-zip.sh b/bin/create-preinstalled-zip.sh new file mode 100755 index 0000000000..631cbb9d5f --- /dev/null +++ b/bin/create-preinstalled-zip.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Packages to pack +PACKAGES=("packages/collector" "packages/core" "packages/shared-metrics") + +# staging dir for tgz files +# Try to name the staging dir using the collector package version; fall back to mktemp if not available +COLLECTOR_PKG_JSON="$REPO_ROOT/packages/collector/package.json" +if command -v node >/dev/null 2>&1 && [ -f "$COLLECTOR_PKG_JSON" ]; then + COLLECTOR_VERSION=$(node -e "console.log(require(process.argv[1]).version)" "$COLLECTOR_PKG_JSON") + STAGING_DIR="/tmp/instana-pack-${COLLECTOR_VERSION}" + # avoid clobbering an existing dir by appending a timestamp + if [ -d "$STAGING_DIR" ]; then + STAGING_DIR="${STAGING_DIR}-$(date +%s)" + fi + mkdir -p "$STAGING_DIR" +else + STAGING_DIR=$(mktemp -d "/tmp/instana-pack-XXXX") +fi +trap 'rm -rf "$STAGING_DIR"' EXIT + +DEST="$HOME/dev/instana/zips-nodejs-tracer" +mkdir -p "$DEST" + +# Pack all packages and move tgz files to STAGING_DIR +for PKG in "${PACKAGES[@]}"; do + PKG_DIR="$REPO_ROOT/$PKG" + echo "Packing package: $PKG_DIR" + cd "$PKG_DIR" + + PKG_BASENAME=$(basename "$PKG_DIR") + + # remove previous tgz files in package dir + rm -f ${PKG_BASENAME}-*.tgz || true + + TGZ_OUTPUT=$(npm pack --silent 2>/dev/null || true) + TGZ=$(echo "$TGZ_OUTPUT" | head -n1) + + if [ -z "$TGZ" ] || [ ! -f "$TGZ" ]; then + TGZ=$(ls -1t ${PKG_BASENAME}-*.tgz 2>/dev/null | head -n1 || true) + fi + + if [ -z "$TGZ" ] || [ ! -f "$TGZ" ]; then + echo "ERROR: could not find generated .tgz file for $PKG" >&2 + exit 1 + fi + + # move and normalize name in staging dir + STAGED_TGZ="$STAGING_DIR/${PKG_BASENAME}.tgz" + mv "$TGZ" "$STAGED_TGZ" + echo "Moved $TGZ to $STAGED_TGZ" +done + +# Only unpack collector, then install its production deps +COLLECTOR_TGZ="$STAGING_DIR/collector.tgz" +if [ ! -f "$COLLECTOR_TGZ" ]; then + echo "ERROR: collector tgz not found in staging dir" >&2 + exit 1 +fi + +TMPDIR=$(mktemp -d "/tmp/package-collector-XXXX") +echo "Using temp dir $TMPDIR" + +echo "Copying $COLLECTOR_TGZ to $TMPDIR/" +cp "$COLLECTOR_TGZ" "$TMPDIR/" + +cd "$TMPDIR" + +echo "Extracting collector package..." +tar -xzf "$(basename "$COLLECTOR_TGZ")" + +cd package + +echo "Installing collector production dependencies (omitting optional and dev)..." +npm install --omit=optional --omit=dev + +# Now install core and shared-metrics into the extracted collector via the tgz files +CORE_TGZ="$STAGING_DIR/core.tgz" +SHARED_TGZ="$STAGING_DIR/shared-metrics.tgz" + +INSTALL_ARGS=() +if [ -f "$CORE_TGZ" ]; then + INSTALL_ARGS+=("$CORE_TGZ") +else + echo "WARNING: core tgz not found, skipping" >&2 +fi +if [ -f "$SHARED_TGZ" ]; then + INSTALL_ARGS+=("$SHARED_TGZ") +else + echo "WARNING: shared-metrics tgz not found, skipping" >&2 +fi + +if [ ${#INSTALL_ARGS[@]} -gt 0 ]; then + echo "Installing core and shared-metrics from tgz files (omitting optional and dev)..." + + # Print the exact command that will be executed + echo -n "Command: npm install --omit=optional --omit=dev" + for _p in "${INSTALL_ARGS[@]}"; do + echo -n " $_p" + done + echo + + # Execute the install using the array to preserve argument boundaries + npm install --omit=optional --omit=dev "${INSTALL_ARGS[@]}" +else + echo "No additional tgz packages to install" +fi + +# Read version and name from package.json +if command -v node >/dev/null 2>&1; then + VERSION=$(node -e "console.log(require('./package.json').version)") + NAME=$(node -e "console.log(require('./package.json').name.replace('@instana/',''))") +else + echo "ERROR: node is required to read package.json version" >&2 + rm -rf "$TMPDIR" + exit 1 +fi + +# Allow a custom postfix passed as first script argument or via ZIP_POSTFIX env var +# Usage: ./create-preinstalled-zip.sh mypostfix +POSTFIX="${1:-${ZIP_POSTFIX:-}}" +DATE=$(date +%d-%m-%Y) +if [ -n "$POSTFIX" ]; then + ZIPNAME="instana-${NAME}-${VERSION}-${DATE}-${POSTFIX}.zip" +else + ZIPNAME="instana-${NAME}-${VERSION}-${DATE}.zip" +fi + +echo "Creating zip $ZIPNAME..." +zip -r "$TMPDIR/$ZIPNAME" . >/dev/null + +echo "Moving $ZIPNAME to $DEST" +mv "$TMPDIR/$ZIPNAME" "$DEST/" + +echo "Cleaning up $TMPDIR" +rm -rf "$TMPDIR" + +echo "Done. Zip is located at: $DEST/$ZIPNAME" + +exit 0 diff --git a/package-lock.json b/package-lock.json index e5b1e5198b..4ed95e37ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15316,6 +15316,16 @@ "node": ">=14" } }, + "node_modules/@opentelemetry/api-metrics": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-metrics/-/api-metrics-0.27.0.tgz", + "integrity": "sha512-tB79288bwjkdhPNpw4UdOEy3bacVwtol6Que7cAu8KEJ9ULjRfSiwpYEwJY/oER3xZ7zNFz0uiJ7N1jSiotpVA==", + "deprecated": "Please use @opentelemetry/api >= 1.3.0", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/context-async-hooks": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.22.0.tgz", @@ -19403,6 +19413,15 @@ "@types/node": "*" } }, + "node_modules/@types/bson": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.5.tgz", + "integrity": "sha512-vVLwMUqhYJSQ/WKcE60eFqcyuWse5fGH+NMAXHuKrUAPoryq3ATxk5o4bgYNtg5aOM4APVg7Hnb3ASqUYG0PKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/bunyan": { "version": "1.8.9", "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.9.tgz", @@ -19818,6 +19837,16 @@ "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", "dev": true }, + "node_modules/@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "license": "MIT", + "dependencies": { + "@types/bson": "*", + "@types/node": "*" + } + }, "node_modules/@types/morgan": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz", @@ -30051,7 +30080,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "devOptional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -31413,7 +31441,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "dependencies": { "function-bind": "^1.1.1" }, @@ -32570,7 +32597,6 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -36117,9 +36143,10 @@ } }, "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" }, "node_modules/module-not-found-error": { "version": "1.0.1", @@ -39477,8 +39504,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-platform": { "version": "0.11.15", @@ -42522,7 +42548,6 @@ "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -44887,7 +44912,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -48628,6 +48652,7 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0", "@opentelemetry/context-async-hooks": "1.25.0", "@opentelemetry/instrumentation-fs": "0.28.0", + "@opentelemetry/instrumentation-mongodb": "0.62.0", "@opentelemetry/instrumentation-oracledb": "0.34.0", "@opentelemetry/instrumentation-restify": "0.53.0", "@opentelemetry/instrumentation-socket.io": "0.54.0", @@ -48676,6 +48701,38 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, + "packages/core/node_modules/@opentelemetry/instrumentation": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.27.0.tgz", + "integrity": "sha512-dUwY/VoDptdK8AYigwS3IKblG+unV5xIdV4VQKy+nX5aT3f7vd5PMYs4arCQSYLbLRe0s7GxK6S9dtjai/TsHQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-metrics": "0.27.0", + "require-in-the-middle": "^5.0.3", + "semver": "^7.3.2", + "shimmer": "^1.2.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "packages/core/node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.28.0.tgz", + "integrity": "sha512-jknWMMRPEp9rcnsr/K4HjF9NyWijGl4dmHeHU5Iqu3EShWazuADuGVdE1NasX1EdZN9tGRiE+H95v79EQiNynQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.27.0", + "@opentelemetry/semantic-conventions": "^1.0.0", + "@types/mongodb": "3.6.20" + }, + "engines": { + "node": ">=8.5.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.2" + } + }, "packages/core/node_modules/@opentelemetry/resources": { "version": "1.25.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.0.tgz", @@ -48727,6 +48784,20 @@ "module-details-from-path": "^1.0.3" } }, + "packages/core/node_modules/require-in-the-middle": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.2.0.tgz", + "integrity": "sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=6" + } + }, "packages/core/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", diff --git a/packages/collector/src/announceCycle/agentready.js b/packages/collector/src/announceCycle/agentready.js index 62c89dfd58..f7ab7be9dc 100644 --- a/packages/collector/src/announceCycle/agentready.js +++ b/packages/collector/src/announceCycle/agentready.js @@ -152,14 +152,17 @@ function enter(_ctx) { // TODO: Add an EventEmitter functionality for the current process // such as `instana.on('instana.collector.initialized')`. // eslint-disable-next-line no-unused-expressions - process?.send?.('instana.collector.initialized'); + if (process.env.INSTANA_IPC_ENABLED === 'true') { + logger?.debug('IPC enabled.'); + process?.send?.('instana.collector.initialized'); - if (!isMainThread) { - const { parentPort } = require('worker_threads'); + if (!isMainThread) { + const { parentPort } = require('worker_threads'); - if (parentPort) { - // CASE: This is for the worker thread if available. - parentPort.postMessage('instana.collector.initialized'); + if (parentPort) { + // CASE: This is for the worker thread if available. + parentPort.postMessage('instana.collector.initialized'); + } } } } diff --git a/packages/collector/src/index.js b/packages/collector/src/index.js index 7b40eaa7ae..0558581510 100644 --- a/packages/collector/src/index.js +++ b/packages/collector/src/index.js @@ -100,7 +100,10 @@ function init(userConfig = {}) { } if (collectorIndexCacheKey) { - process?.send?.('instana.collector.initialized'); + if (process.env.INSTANA_IPC_ENABLED === 'true') { + logger?.debug('IPC enabled.'); + process?.send?.('instana.collector.initialized'); + } return require.cache[collectorIndexCacheKey].exports; } else { diff --git a/packages/collector/test/apps/expressControls.js b/packages/collector/test/apps/expressControls.js index 5421a0523c..497d864fd3 100644 --- a/packages/collector/test/apps/expressControls.js +++ b/packages/collector/test/apps/expressControls.js @@ -30,6 +30,7 @@ exports.start = function start(opts = {}, retryTime = null) { env.TRACING_ENABLED = opts.enableTracing !== false; env.STACK_TRACE_LENGTH = opts.stackTraceLength || 0; env.APP_USES_HTTPS = opts.appUsesHttps === true; + env.INSTANA_IPC_ENABLED = 'true'; if (env.APP_USES_HTTPS) { // CASE: target app uses HTTPS (self cert) diff --git a/packages/collector/test/test_util/ProcessControls.js b/packages/collector/test/test_util/ProcessControls.js index 43de07e843..05aa0805d6 100644 --- a/packages/collector/test/test_util/ProcessControls.js +++ b/packages/collector/test/test_util/ProcessControls.js @@ -151,7 +151,8 @@ class ProcessControls { INSTANA_FIRE_MONITORING_EVENT_DURATION_IN_MS: 500, INSTANA_RETRY_AGENT_CONNECTION_IN_MS: 500, APP_USES_HTTPS: this.appUsesHttps ? 'true' : 'false', - INSTANA_DISABLE_USE_OPENTELEMETRY: !this.enableOtelIntegration + INSTANA_DISABLE_USE_OPENTELEMETRY: !this.enableOtelIntegration, + INSTANA_IPC_ENABLED: 'true' }, opts.env ); diff --git a/packages/collector/test/tracing/control_flow/async_await/controls.js b/packages/collector/test/tracing/control_flow/async_await/controls.js index 61fd2dba49..69f2112482 100644 --- a/packages/collector/test/tracing/control_flow/async_await/controls.js +++ b/packages/collector/test/tracing/control_flow/async_await/controls.js @@ -29,7 +29,7 @@ exports.start = opts => { env.UPSTREAM_PORT = opts.expressControls ? opts.expressControls.getPort() : null; env.USE_REQUEST_PROMISE = String(opts.useRequestPromise); - + env.INSTANA_IPC_ENABLED = 'true'; // eslint-disable-next-line no-console console.log( // eslint-disable-next-line max-len diff --git a/packages/collector/test/tracing/logging/misc/controls.js b/packages/collector/test/tracing/logging/misc/controls.js index 021571e668..80421516ac 100644 --- a/packages/collector/test/tracing/logging/misc/controls.js +++ b/packages/collector/test/tracing/logging/misc/controls.js @@ -70,6 +70,7 @@ class AppControls { env.TRACING_ENABLED = opts.enableTracing !== false; env.INSTANA_RETRY_AGENT_CONNECTION_IN_MS = 100; env.PINO_VERSION = opts.PINO_VERSION; + env.INSTANA_IPC_ENABLED = 'true'; if (this.customEnv) { env = Object.assign(env, this.customEnv); diff --git a/packages/collector/test/tracing/messaging/amqp/consumerControls.js b/packages/collector/test/tracing/messaging/amqp/consumerControls.js index 9ff81f1479..f03fca4fec 100644 --- a/packages/collector/test/tracing/messaging/amqp/consumerControls.js +++ b/packages/collector/test/tracing/messaging/amqp/consumerControls.js @@ -22,6 +22,7 @@ exports.registerTestHooks = opts => { env.TRACING_ENABLED = opts.enableTracing !== false; env.AMQPLIB_VERSION = opts.version; env.INSTANA_RETRY_AGENT_CONNECTION_IN_MS = 100; + env.INSTANA_IPC_ENABLED = 'true'; app = spawn('node', [path.join(__dirname, `consumer${opts.apiType}.js`)], { stdio: config.getAppStdio(), diff --git a/packages/collector/test/tracing/messaging/amqp/publisherControls.js b/packages/collector/test/tracing/messaging/amqp/publisherControls.js index b0aa630baa..dab83b0e27 100644 --- a/packages/collector/test/tracing/messaging/amqp/publisherControls.js +++ b/packages/collector/test/tracing/messaging/amqp/publisherControls.js @@ -28,6 +28,7 @@ exports.registerTestHooks = opts => { env.TRACING_ENABLED = opts.enableTracing !== false; env.AMQPLIB_VERSION = opts.version; env.INSTANA_RETRY_AGENT_CONNECTION_IN_MS = 100; + env.INSTANA_IPC_ENABLED = 'true'; app = spawn('node', [path.join(__dirname, `publisher${opts.apiType}.js`)], { stdio: config.getAppStdio(), diff --git a/packages/collector/test/tracing/open_tracing/controls.js b/packages/collector/test/tracing/open_tracing/controls.js index db7e8faf72..52078dc144 100644 --- a/packages/collector/test/tracing/open_tracing/controls.js +++ b/packages/collector/test/tracing/open_tracing/controls.js @@ -26,6 +26,7 @@ exports.registerTestHooks = opts => { appPort = env.APP_PORT; env.TRACING_ENABLED = opts.enableTracing !== false; env.DISABLE_AUTOMATIC_TRACING = opts.automaticTracingEnabled === false; + env.INSTANA_IPC_ENABLED = 'true'; // By default, we test without OpenTelemetry instrumentation enabled // because the test setup is currently broken and not capturing OTEL spans. // TODO: INSTA-62539 diff --git a/packages/collector/test/tracing/opentelemetry/mongodb-app.js b/packages/collector/test/tracing/opentelemetry/mongodb-app.js new file mode 100644 index 0000000000..398b93c29a --- /dev/null +++ b/packages/collector/test/tracing/opentelemetry/mongodb-app.js @@ -0,0 +1,60 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +// NOTE: c8 bug https://github.com/bcoe/c8/issues/166 +process.on('SIGTERM', () => { + process.disconnect(); + process.exit(0); +}); + +require('@instana/collector')(); + +const express = require('express'); +const { MongoClient } = require('mongodb'); +const port = require('../../test_util/app-port')(); + +const app = express(); +let db; +let collection; +let connected = false; + +const connectString = `mongodb://${process.env.MONGODB || '127.0.0.1:27017'}/testdb`; + +(async () => { + try { + const client = new MongoClient(connectString); + await client.connect(); + db = client.db('testdb'); + collection = db.collection('testdocs'); + connected = true; + console.log('Connected to MongoDB'); + } catch (err) { + console.error('Failed to connect to MongoDB', err); + } +})(); + +app.get('/', (req, res) => { + if (!connected || !db || !collection) { + res.sendStatus(500); + } else { + res.sendStatus(200); + } +}); + +app.get('/insert', async (req, res) => { + console.log('insert'); + try { + const result = await collection.insertOne({ name: 'test', value: 123 }); + res.json(result); + } catch (err) { + console.error('Failed to insert document', err); + res.status(500).json({ error: err.message }); + } +}); + +app.listen(port, () => { + console.log(`MongoDB App listening on port: ${port}`); +}); diff --git a/packages/collector/test/tracing/opentelemetry/test.js b/packages/collector/test/tracing/opentelemetry/test.js index d95a48dfa1..c960d2305c 100644 --- a/packages/collector/test/tracing/opentelemetry/test.js +++ b/packages/collector/test/tracing/opentelemetry/test.js @@ -833,6 +833,66 @@ mochaSuiteFn('opentelemetry tests', function () { )); }); }); + + describe('mongodb', function () { + globalAgent.setUpCleanUpHooks(); + const agentControls = globalAgent.instance; + + let controls; + + before(async () => { + controls = new ProcessControls({ + appPath: path.join(__dirname, './mongodb-app.js'), + useGlobalAgent: true, + cwd: __dirname, + enableOtelIntegration: true, + env: { OTEL_API_VERSION: version } + }); + + await controls.startAndWaitForAgentConnection(); + }); + + beforeEach(async () => { + await agentControls.clearReceivedTraceData(); + }); + + after(async () => { + await controls.stop(); + }); + + it('should trace', async () => { + await controls.sendRequest({ + method: 'GET', + path: '/insert' + }); + + await retry(async () => { + const spans = await agentControls.getSpans(); + expect(spans.length).to.equal(2); + + const httpEntry = verifyHttpRootEntry({ + spans, + apiPath: '/insert', + pid: String(controls.getPid()) + }); + + verifyExitSpan({ + spanName: 'otel', + spans, + parent: httpEntry, + withError: false, + pid: String(controls.getPid()), + dataProperty: 'tags', + extraTests: span => { + expect(span.data.operation).to.equal('mongodb'); + expect(span.data.tags.name).to.contain('insert'); + expect(span.data.tags['db.system']).to.eql('mongodb'); + checkTelemetryResourceAttrs(span); + } + }); + }); + }); + }); }); }); diff --git a/packages/collector/test/tracing/protocols/http/proxy/expressProxyControls.js b/packages/collector/test/tracing/protocols/http/proxy/expressProxyControls.js index 350b798196..7a8c89916e 100644 --- a/packages/collector/test/tracing/protocols/http/proxy/expressProxyControls.js +++ b/packages/collector/test/tracing/protocols/http/proxy/expressProxyControls.js @@ -57,6 +57,7 @@ exports.start = async (opts = {}) => { env.STACK_TRACE_LENGTH = opts.stackTraceLength || 0; env.INSTANA_RETRY_AGENT_CONNECTION_IN_MS = 100; env.EXPRESS_VERSION = opts.EXPRESS_VERSION; + env.INSTANA_IPC_ENABLED = 'true'; expressProxyApp = spawn('node', [path.join(__dirname, 'expressProxy.js')], { stdio: config.getAppStdio(), diff --git a/packages/core/package.json b/packages/core/package.json index 7320a0e509..2f273bb242 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -64,6 +64,7 @@ "@opentelemetry/instrumentation-restify": "0.53.0", "@opentelemetry/instrumentation-socket.io": "0.54.0", "@opentelemetry/instrumentation-tedious": "0.27.0", + "@opentelemetry/instrumentation-mongodb": "0.62.0", "@opentelemetry/sdk-trace-base": "1.25.0", "cls-bluebird": "^2.1.0", "import-in-the-middle": "2.0.0", diff --git a/packages/core/src/tracing/instrumentation/databases/mongodb.js b/packages/core/src/tracing/instrumentation/databases/mongodb.js index 15ca92270c..22b5d86098 100644 --- a/packages/core/src/tracing/instrumentation/databases/mongodb.js +++ b/packages/core/src/tracing/instrumentation/databases/mongodb.js @@ -7,7 +7,6 @@ const shimmer = require('../../shimmer'); -const hook = require('../../../util/hook'); const tracingUtil = require('../../tracingUtil'); const constants = require('../../constants'); const cls = require('../../cls'); @@ -34,13 +33,58 @@ exports.batchable = true; exports.init = function init() { // unified topology layer - hook.onFileLoad(/\/mongodb\/lib\/cmap\/connection\.js/, instrumentCmapConnection); + // hook.onFileLoad(/\/mongodb\/lib\/cmap\/connection\.js/, instrumentCmapConnection); // mongodb >= 3.3.x, legacy topology layer - hook.onFileLoad(/\/mongodb\/lib\/core\/connection\/pool\.js/, instrumentLegacyTopologyPool); + // hook.onFileLoad(/\/mongodb\/lib\/core\/connection\/pool\.js/, instrumentLegacyTopologyPool); // mongodb < 3.3.x, legacy topology layer - hook.onFileLoad(/\/mongodb-core\/lib\/connection\/pool\.js/, instrumentLegacyTopologyPool); + // hook.onFileLoad(/\/mongodb-core\/lib\/connection\/pool\.js/, instrumentLegacyTopologyPool); + + tryPatchMongoDBDirectly(); + + setImmediate(() => { + tryPatchMongoDBDirectly(); + }); }; +function tryPatchMongoDBDirectly() { + try { + require('mongodb'); + } catch (e) { + return; + } + + Object.keys(require.cache).forEach(filename => { + const normalizedPath = filename.replace(/\\/g, '/'); + + if (normalizedPath.includes('/mongodb/lib/cmap/connection.js')) { + try { + const connection = require.cache[filename].exports; + instrumentCmapConnection(connection); + } catch (e) { + // ignore + } + } + + if (normalizedPath.includes('/mongodb/lib/core/connection/pool.js')) { + try { + const Pool = require.cache[filename].exports; + instrumentLegacyTopologyPool(Pool); + } catch (e) { + // ignore + } + } + + if (normalizedPath.includes('/mongodb-core/lib/connection/pool.js')) { + try { + const Pool = require.cache[filename].exports; + instrumentLegacyTopologyPool(Pool); + } catch (e) { + // ignore + } + } + }); +} + function instrumentCmapConnection(connection) { if (connection.Connection && connection.Connection.prototype) { // v4, v5 diff --git a/packages/core/src/tracing/opentelemetry-instrumentations/mongodb.js b/packages/core/src/tracing/opentelemetry-instrumentations/mongodb.js new file mode 100644 index 0000000000..4ec6d51de7 --- /dev/null +++ b/packages/core/src/tracing/opentelemetry-instrumentations/mongodb.js @@ -0,0 +1,15 @@ +/* + * (c) Copyright IBM Corp. 2026 + */ + +'use strict'; + +module.exports.init = () => { + const { MongoDBInstrumentation } = require('@opentelemetry/instrumentation-mongodb'); + + const instrumentation = new MongoDBInstrumentation(); + + if (!instrumentation.getConfig().enabled) { + instrumentation.enable(); + } +}; diff --git a/packages/core/src/tracing/opentelemetry-instrumentations/wrap.js b/packages/core/src/tracing/opentelemetry-instrumentations/wrap.js index 7908db4048..87445191a2 100644 --- a/packages/core/src/tracing/opentelemetry-instrumentations/wrap.js +++ b/packages/core/src/tracing/opentelemetry-instrumentations/wrap.js @@ -22,6 +22,7 @@ const instrumentations = { '@opentelemetry/instrumentation-socket.io': { name: 'socket.io' }, '@opentelemetry/instrumentation-tedious': { name: 'tedious' }, '@opentelemetry/instrumentation-oracledb': { name: 'oracle' } + // '@opentelemetry/instrumentation-mongodb': { name: 'mongodb' } }; // NOTE: using a logger might create a recursive execution @@ -39,11 +40,19 @@ module.exports.init = (_config, cls) => { value.module = instrumentation; }); - const transformToInstanaSpan = otelSpan => { + const transformedSpans = new WeakSet(); + + const transformToInstanaSpan = (otelSpan, isAlreadyEnded = false) => { if (!otelSpan || !otelSpan.instrumentationLibrary) { return; } + if (transformedSpans.has(otelSpan)) { + return; + } + + transformedSpans.add(otelSpan); + const targetInstrumentionName = otelSpan.instrumentationLibrary.name; let kind = constants.EXIT; @@ -93,17 +102,37 @@ module.exports.init = (_config, cls) => { resource: otelSpan.resource.attributes }; - const origEnd = otelSpan.end; - otelSpan.end = function instanaOnEnd() { + if (isAlreadyEnded) { instanaSpan.transmit(); - return origEnd.apply(this, arguments); - }; + } else { + const origEnd = otelSpan.end; + otelSpan.end = function instanaOnEnd() { + instanaSpan.transmit(); + return origEnd.apply(this, arguments); + }; + } }); } catch (e) { // ignore for now } }; + class InstanaSpanProcessor { + onStart() {} + + onEnd(span) { + transformToInstanaSpan(span, true); + } + + shutdown() { + return Promise.resolve(); + } + + forceFlush() { + return Promise.resolve(); + } + } + /** * OpenTelemetry initializes with a ProxyTracerProvider as the default global tracer provider * when no actual provider has been registered yet. Initially, all tracer requests are routed @@ -123,6 +152,8 @@ module.exports.init = (_config, cls) => { const provider = new BasicTracerProvider(); const contextManager = new AsyncHooksContextManager(); + provider.addSpanProcessor(new InstanaSpanProcessor()); + api.trace.setGlobalTracerProvider(provider); api.context.setGlobalContextManager(contextManager); diff --git a/packages/core/src/util/requireHook.js b/packages/core/src/util/requireHook.js index c57c723b23..54ed623dd7 100644 --- a/packages/core/src/util/requireHook.js +++ b/packages/core/src/util/requireHook.js @@ -66,18 +66,20 @@ function patchedModuleLoad(moduleName) { // However, when an ESM library imports a CommonJS package, our requireHook is triggered. // For native ESM libraries the iitmHook is triggered. if (path.isAbsolute(moduleName) && ['.node', '.json', '.ts'].indexOf(path.extname(moduleName)) === -1) { + // Normalize Windows paths (backslashes) to forward slashes for regex matching + const normalizedModuleName = moduleName.replace(/\\/g, '/'); // EDGE CASE for ESM: mysql2/promise.js - if (moduleName.indexOf('node_modules/mysql2/promise.js') !== -1) { + if (normalizedModuleName.indexOf('node_modules/mysql2/promise.js') !== -1) { moduleName = 'mysql2/promise'; } else { // e.g. path is node_modules/@elastic/elasicsearch/index.js - let match = moduleName.match(/node_modules\/(@.*?(?=\/)\/.*?(?=\/))/); + let match = normalizedModuleName.match(/node_modules\/(@.*?(?=\/)\/.*?(?=\/))/); if (match && match.length > 1) { moduleName = match[1]; } else { // e.g. path is node_modules/mysql/lib/index.js - match = moduleName.match(/node_modules\/(.*?(?=\/))/); + match = normalizedModuleName.match(/node_modules\/(.*?(?=\/))/); if (match && match.length > 1) { moduleName = match[1]; @@ -145,8 +147,11 @@ function patchedModuleLoad(moduleName) { } if (!cacheEntry.byFileNamePatternTransformersApplied) { + // Normalize Windows paths (backslashes) to forward slashes for regex pattern matching + // This ensures patterns with forward slashes (like /\/mongodb-core\/lib\/connection\/pool\.js/) work on Windows + const normalizedFilename = filename.replace(/\\/g, '/'); for (let i = 0; i < byFileNamePatternTransformers.length; i++) { - if (byFileNamePatternTransformers[i].pattern.test(filename)) { + if (byFileNamePatternTransformers[i].pattern.test(normalizedFilename)) { cacheEntry.moduleExports = byFileNamePatternTransformers[i].fn(cacheEntry.moduleExports, filename) || cacheEntry.moduleExports; } @@ -191,6 +196,15 @@ exports.buildFileNamePattern = function buildFileNamePattern(arr) { return new RegExp(`${arr.reduce(buildFileNamePatternReducer, '')}$`); }; +/** + * Escapes special regex characters in a string + * @param {string} str + * @returns {string} + */ +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** * @param {string} agg * @param {string} pathSegment @@ -198,8 +212,11 @@ exports.buildFileNamePattern = function buildFileNamePattern(arr) { */ function buildFileNamePatternReducer(agg, pathSegment) { if (agg.length > 0) { - agg += `\\${path.sep}`; + // Always use forward slashes in patterns since we normalize filenames to forward slashes + // This ensures patterns work consistently on both Windows and Unix systems + agg += '\\/'; } - agg += pathSegment; + // Escape special regex characters in path segments (e.g., '.' in 'express.js' should be '\.') + agg += escapeRegex(pathSegment); return agg; } diff --git a/packages/core/test/util/require_hook/requireHook_test.js b/packages/core/test/util/require_hook/requireHook_test.js index ab0df8e4f1..427c580853 100644 --- a/packages/core/test/util/require_hook/requireHook_test.js +++ b/packages/core/test/util/require_hook/requireHook_test.js @@ -183,12 +183,409 @@ describe('util/requireHook', () => { const pattern = requireHook.buildFileNamePattern(['node_modules', 'express', 'lib', 'express.js']); requireHook.onFileLoad(pattern, hook); - expect(require('express')).to.be.a('function'); + // Require the specific file that matches the pattern, not just 'express' + // which loads index.js. This ensures the pattern is tested against the actual file. + expect(require('express/lib/express')).to.be.a('function'); expect(hook.callCount).to.equal(1); expect(hook.getCall(0).args[0]).to.be.a('function'); expect(hook.getCall(0).args[0].name).to.equal('createApplication'); }); }); + + it('must handle Windows paths with backslashes in onFileLoad patterns', () => { + const testModule = { test: 'module' }; + const windowsPath = + 'C:\\Users\\johndoe\\Desktop\\code\\mongo-app\\node_modules\\mongodb-core\\lib\\connection\\pool.js'; + + // Create a function that will be captured as origLoad + const originalLoad = function () { + return testModule; + }; + + // Create a mock Module that will be used when requireHook loads + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return windowsPath; + } + }; + + // Use proxyquire to inject the mocked Module before requireHook loads + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + // Use a pattern similar to mongodb.js that expects forward slashes + requireHookWithMock.onFileLoad(/\/mongodb-core\/lib\/connection\/pool\.js/, hook); + + // After init(), mockModule._load is now patchedModuleLoad + // Call it with a Windows absolute path - this should trigger the pattern match + const result = mockModule._load( + 'C:\\Users\\johndoe\\Desktop\\code\\mongo-app\\node_modules\\mongodb-core\\lib\\connection\\pool.js' + ); + + // Verify the hook was called despite Windows path separators + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + expect(hook.getCall(0).args[1]).to.equal(windowsPath); + expect(result).to.deep.equal(testModule); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must extract module name correctly from Windows paths in onModuleLoad', () => { + const path = require('path'); + const testMssqlModule = { test: 'mssql-module' }; + // Use a Windows path that will be normalized and matched + // On non-Windows systems, path.isAbsolute() may return false for Windows paths, + // so we need to ensure the path is treated as absolute in the test + const windowsPath = 'C:\\Users\\johndoe\\Desktop\\code\\mongo-app\\node_modules\\mssql\\lib\\index.js'; + const windowsModuleName = 'C:\\Users\\johndoe\\Desktop\\code\\mongo-app\\node_modules\\mssql\\lib\\index.js'; + + // Store the originalLoad function reference so we can ensure same object is returned + let loadCallCount = 0; + const originalLoad = function () { + loadCallCount++; + // Must return the same object reference each time to pass cache check + return testMssqlModule; + }; + + // Create a mock Module that will be used when requireHook loads + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + // _resolveFilename receives the same arguments as _load was called with + return windowsPath; + } + }; + + // Mock path.isAbsolute to return true for Windows paths (even on non-Windows systems) + const pathMock = { + isAbsolute: function (p) { + // Treat Windows absolute paths (C:\, D:\, etc.) as absolute + if (/^[A-Za-z]:[\\/]/.test(p)) { + return true; + } + return path.isAbsolute(p); + }, + extname: path.extname, + sep: path.sep + }; + + // Use proxyquire to inject the mocked Module and path before requireHook loads + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule, + path: pathMock + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + // Register hook for mssql module (similar to mssql.js) + requireHookWithMock.onModuleLoad('mssql', hook); + + // After init(), mockModule._load is replaced with patchedModuleLoad + // When we call it, patchedModuleLoad will: + // 1. Extract module name from Windows path: 'C:\...\node_modules\mssql\lib\index.js' -> 'mssql' + // 2. Call origLoad (our mock) which returns testMssqlModule + // 3. Call _resolveFilename which returns windowsPath + // 4. Check byModuleNameTransformers['mssql'] and call the hook + const result = mockModule._load(windowsModuleName); + + // Verify origLoad was called + expect(loadCallCount).to.equal(1); + // Verify the hook was called (module name 'mssql' should be extracted from Windows path) + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testMssqlModule); + expect(result).to.deep.equal(testMssqlModule); + + requireHookWithMock.teardownForTestPurposes(); + }); + + describe('moduleName handling (relative, absolute, module name)', () => { + it('must handle relative paths on Unix systems', () => { + const testModule = { test: 'relative-module' }; + const relativePath = './testModuleA'; + const resolvedPath = '/Users/testuser/project/testModuleA.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return resolvedPath; + } + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onFileLoad(/testModuleA/, hook); + + // Call with relative path - should work because _resolveFilename returns absolute path + mockModule._load(relativePath); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + expect(hook.getCall(0).args[1]).to.equal(resolvedPath); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle relative paths on Windows systems', () => { + const testModule = { test: 'relative-module' }; + const relativePath = '.\\testModuleA'; + const resolvedPath = 'C:\\Users\\testuser\\project\\testModuleA.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return resolvedPath; + } + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onFileLoad(/testModuleA/, hook); + + // Call with Windows relative path - should work + mockModule._load(relativePath); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + expect(hook.getCall(0).args[1]).to.equal(resolvedPath); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle module names that resolve to absolute paths on Unix', () => { + const testModule = { test: 'mssql-module' }; + const moduleName = 'mssql'; + const resolvedPath = '/Users/testuser/project/node_modules/mssql/lib/index.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return resolvedPath; + } + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onModuleLoad('mssql', hook); + + // Call with module name - should extract 'mssql' from resolved path + mockModule._load(moduleName); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle module names that resolve to absolute paths on Windows', () => { + const path = require('path'); + const testModule = { test: 'mssql-module' }; + const moduleName = 'mssql'; + const resolvedPath = 'C:\\Users\\testuser\\project\\node_modules\\mssql\\lib\\index.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return resolvedPath; + } + }; + + const pathMock = { + isAbsolute: function (p) { + if (/^[A-Za-z]:[\\/]/.test(p)) { + return true; + } + return path.isAbsolute(p); + }, + extname: path.extname, + sep: path.sep + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule, + path: pathMock + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onModuleLoad('mssql', hook); + + // Call with module name - should extract 'mssql' from Windows resolved path + mockModule._load(moduleName); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle absolute Unix paths in onFileLoad', () => { + const testModule = { test: 'unix-module' }; + const absolutePath = '/Users/testuser/project/node_modules/mongodb-core/lib/connection/pool.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return absolutePath; + } + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onFileLoad(/\/mongodb-core\/lib\/connection\/pool\.js/, hook); + + // Call with Unix absolute path + mockModule._load(absolutePath); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + expect(hook.getCall(0).args[1]).to.equal(absolutePath); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle absolute Windows paths in onFileLoad', () => { + const testModule = { test: 'windows-module' }; + const windowsPath = 'C:\\Users\\testuser\\project\\node_modules\\mongodb-core\\lib\\connection\\pool.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return windowsPath; + } + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onFileLoad(/\/mongodb-core\/lib\/connection\/pool\.js/, hook); + + // Call with Windows absolute path - should normalize and match + mockModule._load(windowsPath); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + expect(hook.getCall(0).args[1]).to.equal(windowsPath); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle scoped module names (e.g., @scope/package) on Unix', () => { + const testModule = { test: 'scoped-module' }; + const moduleName = '@elastic/elasticsearch'; + const resolvedPath = '/Users/testuser/project/node_modules/@elastic/elasticsearch/index.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return resolvedPath; + } + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onModuleLoad('@elastic/elasticsearch', hook); + + // Call with scoped module name - should extract '@elastic/elasticsearch' from resolved path + mockModule._load(moduleName); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + + requireHookWithMock.teardownForTestPurposes(); + }); + + it('must handle scoped module names (e.g., @scope/package) on Windows', () => { + const path = require('path'); + const testModule = { test: 'scoped-module' }; + const moduleName = '@elastic/elasticsearch'; + const resolvedPath = 'C:\\Users\\testuser\\project\\node_modules\\@elastic\\elasticsearch\\index.js'; + + const originalLoad = function () { + return testModule; + }; + + const mockModule = { + _load: originalLoad, + _resolveFilename: function () { + return resolvedPath; + } + }; + + const pathMock = { + isAbsolute: function (p) { + if (/^[A-Za-z]:[\\/]/.test(p)) { + return true; + } + return path.isAbsolute(p); + }, + extname: path.extname, + sep: path.sep + }; + + const requireHookWithMock = proxyquire('../../../src/util/requireHook', { + module: mockModule, + path: pathMock + }); + + requireHookWithMock.init({ logger: testUtils.createFakeLogger() }); + requireHookWithMock.onModuleLoad('@elastic/elasticsearch', hook); + + // Call with scoped module name on Windows - should extract '@elastic/elasticsearch' + mockModule._load(moduleName); + + expect(hook.callCount).to.equal(1); + expect(hook.getCall(0).args[0]).to.deep.equal(testModule); + + requireHookWithMock.teardownForTestPurposes(); + }); + }); }); });