Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/collector/test/nativeModuleRetry/preinstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash

#######################################
# (c) Copyright IBM Corp. 2025
#######################################

cd "$(dirname "$0")/../../../collector"
echo "Running npm pack in $(pwd)"
npm pack

version=$(node -p "require('./package.json').version")
tarball="instana-collector-${version}.tgz"
cp "./${tarball}" "./test/nativeModuleRetry/collector.tgz"

cd "$(dirname "$0")/../core"
echo "Running npm pack in $(pwd)"
npm pack

version=$(node -p "require('./package.json').version")
tarball="instana-core-${version}.tgz"
cp "./${tarball}" "../collector/test/nativeModuleRetry/core.tgz"

cd "$(dirname "$0")/../shared-metrics"
echo "Running npm pack in $(pwd)"
npm pack

version=$(node -p "require('./package.json').version")
tarball="instana-shared-metrics-${version}.tgz"
cp "./${tarball}" "../collector/test/nativeModuleRetry/shared-metrics.tgz"
30 changes: 4 additions & 26 deletions packages/collector/test/nativeModuleRetry/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,42 +94,20 @@ mochaSuiteFn('retry loading native addons', function () {
before(async () => {
execSync(`rm -rf ${tmpFolder}`, { cwd: __dirname, stdio: 'inherit' });
execSync(`mkdir -p ${tmpFolder}`, { cwd: __dirname, stdio: 'inherit' });

execSync(`cp app.js ${tmpFolder}/`, { cwd: __dirname, stdio: 'inherit' });

// eslint-disable-next-line no-console
console.log('Running npm install in', tmpFolder);
execSync('rm -rf node_modules', { cwd: tmpFolder, stdio: 'inherit' });

const copath = path.join(__dirname, '..', '..', '..', 'collector');
runCommandSync('npm pack', copath);

const coversion = require(`${copath}/package.json`).version;
runCommandSync(
`npm install --production --no-optional --no-audit ${copath}/instana-collector-${coversion}.tgz`,
tmpFolder
);

// NOTE: Override the core npm dependency with the local code base
const corepath = path.join(__dirname, '..', '..', '..', 'core');
runCommandSync('npm pack', corepath);

const coreversion = require(`${copath}/package.json`).version;
runCommandSync(
`npm install --prefix ./ --production --no-optional --no-audit ${corepath}/instana-core-${coreversion}.tgz`,
tmpFolder
);

// Install the shared metrics module
const sharedMetricsPath = path.join(__dirname, '..', '..', '..', 'shared-metrics');
runCommandSync('npm pack', sharedMetricsPath);
execSync('./preinstall.sh', { cwd: __dirname, stdio: 'inherit' });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will outsource to main. Test was failing because shared-metrics was not installed from local folder - it took the version from NPM - this does not work if you do refactorings (such as removing fns)


const sharedMetricsVersion = require(`${copath}/package.json`).version;
runCommandSync(`npm install --no-save --no-package-lock --prefix ./ ${__dirname}/core.tgz`, tmpFolder);
runCommandSync(
// eslint-disable-next-line max-len
`npm install --prefix ./ --production --no-optional --no-audit ${sharedMetricsPath}/instana-shared-metrics-${sharedMetricsVersion}.tgz`,
`npm install --no-save --no-package-lock --prefix ./ ${__dirname}/shared-metrics.tgz`,
tmpFolder
);
runCommandSync(`npm install --no-save --no-package-lock --prefix ./ ${__dirname}/collector.tgz`, tmpFolder);

// Remove the target c++ module
execSync(`rm -rf node_modules/${opts.name}`, { cwd: tmpFolder, stdio: 'inherit' });
Expand Down
42 changes: 42 additions & 0 deletions packages/collector/test/tracing/misc/invalid_app/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* (c) Copyright IBM Corp. 2025
*/

'use strict';

const path = require('path');
const childProcess = require('child_process');
const expect = require('chai').expect;
const supportedVersion = require('@instana/core').tracing.supportedVersion;
const config = require('../../../../../core/test/config');
const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : describe.skip;

mochaSuiteFn('tracing/invalidApp', function () {
this.timeout(config.getTestTimeout() * 3);

it('when the collector is required in interactive shell', cb => {
const child = childProcess.spawn(
process.execPath,
['--require', path.join(__dirname, '..', '..', '..', '..', 'src', 'immediate.js')],
{
stdio: 'inherit',
cwd: process.cwd(),
env: process.env
}
);

child.on('error', err => {
cb(err);
});

child.on('exit', (code, signal) => {
expect(signal).to.equal('SIGTERM');
expect(code).to.not.exist;
cb();
});

setTimeout(() => {
child.kill('SIGTERM');
}, 3 * 1000);
});
});
10 changes: 9 additions & 1 deletion packages/collector/test/tracing/opentelemetry/preinstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ npm pack

version=$(node -p "require('./package.json').version")
tarball="instana-core-${version}.tgz"
cp "./${tarball}" "../collector/test/tracing/opentelemetry/core.tgz"
cp "./${tarball}" "../collector/test/tracing/opentelemetry/core.tgz"

cd "$(dirname "$0")/../shared-metrics"
echo "Running npm pack in $(pwd)"
npm pack

version=$(node -p "require('./package.json').version")
tarball="instana-shared-metrics-${version}.tgz"
cp "./${tarball}" "../collector/test/tracing/opentelemetry/shared-metrics.tgz"
6 changes: 6 additions & 0 deletions packages/collector/test/tracing/opentelemetry/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,17 @@ mochaSuiteFn('opentelemetry tests', function () {
if (process.env.INSTANA_TEST_SKIP_INSTALLING_DEPS === 'true') {
return;
}

execSync('npm install --no-save --no-package-lock --prefix ./ ./core.tgz', {
cwd: __dirname,
stdio: 'inherit'
});

execSync('npm install --no-save --no-package-lock --prefix ./ ./shared-metrics.tgz', {
cwd: __dirname,
stdio: 'inherit'
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will outsource to main. Test was failing because shared-metrics was not installed from local folder - it took the version from NPM - this does not work if you do refactorings (such as removing fns)


execSync('npm install --no-save --no-package-lock --prefix ./ ./collector.tgz', {
cwd: __dirname,
stdio: 'inherit'
Expand Down
23 changes: 19 additions & 4 deletions packages/core/src/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@

'use strict';

const fs = require('../uninstrumentedFs');
const path = require('path');
const fs = require('../uninstrumentedFs');
const util = require('../util');

/** @typedef {import('../config').InstanaConfig} InstanaConfig */

/** @type {InstanaConfig} */
let config;

/** @type {import('../core').GenericLogger} */
let logger;

/**
* @param {InstanaConfig} _config
*/
exports.init = _config => {
config = _config;
logger = config.logger;
};

/**
Expand Down Expand Up @@ -65,10 +70,20 @@ exports.registerAdditionalMetrics = function registerAdditionalMetrics(additiona
};

exports.activate = () => {
metricsModules.forEach(metricsModule => {
if (metricsModule.activate) {
metricsModule.activate(config);
util.applicationUnderMonitoring.getMainPackageJsonStartingAtMainModule((err, packageJsonObj) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bigger problem in the codebase:
Any metric requested the pkg json (in parallel).
This created too many log entries and also its logically not right.

Any outer pkg calls core.metrics.activate, which activates all registered metrics.
We request the pkg json here once and then forward a payload to the metric.

if (err) {
logger.warn(
`Failed to determine main package.json. Some metrics might not work. Reason: ${err?.message} ${err?.stack}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log once. And also mention that only some metrics might not work.

);
return;
}

metricsModules.forEach(metricsModule => {
if (metricsModule.activate) {
// @ts-ignore
metricsModule.activate(config, packageJsonObj);
}
});
});
};

Expand Down
46 changes: 30 additions & 16 deletions packages/core/src/util/applicationUnderMonitoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ function isAppInstalledIntoNodeModules() {
return appInstalledIntoNodeModules;
}

const MAX_ATTEMPTS = 5;
const DELAY = 1500;
let attempts = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Centralized retries (see name.js and version.js etc.).
There is no need to retry from all the single metrics.
We still need the retries because of using require/import as arguments (see comment in line 163!)


/**
* Looks for the app's main package.json file, parses it and returns the parsed content. The search is started at
* path.dirname(require.main.filename).
Expand All @@ -50,15 +54,31 @@ function isAppInstalledIntoNodeModules() {
function getMainPackageJsonStartingAtMainModule(cb) {
// NOTE: we already cached the package.json
if (parsedMainPackageJson !== undefined) {
return cb(null, parsedMainPackageJson);
return cb(null, { file: parsedMainPackageJson, path: mainPackageJsonPath });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always return both file + path. Some metrics need the file, some the path.

}

// CASE: customer provided custom package.json path, let's try loading it
if (packageJsonPath) {
return readFile(packageJsonPath, cb);
}

return getMainPackageJsonStartingAtDirectory(null, cb);
return getMainPackageJsonStartingAtDirectory(null, (err, pkg) => {
// CASE: using --require or --import can result in an empty require.main initially
if (err && attempts < MAX_ATTEMPTS) {
attempts++;

logger.warn(
`Could not determine main package.json yet. Retrying ${attempts}/${MAX_ATTEMPTS} after ${DELAY}ms...`
);

return setTimeout(() => {
getMainPackageJsonStartingAtMainModule(cb);
}, DELAY).unref();
}

attempts = 0;
return cb(err, pkg);
});
}

/**
Expand Down Expand Up @@ -110,22 +130,10 @@ function readFile(filePath, cb) {
return cb(e, null);
}

return cb(null, parsedMainPackageJson);
return cb(null, { file: parsedMainPackageJson, path: filePath });
});
}

/**
* Looks for path of the app's main package.json file, starting the search at path.dirname(require.main.filename).
*
* In case the search is successful, the result will be cached for consecutive invocations.
*
* @param {(err: Error, packageJsonPath: string) => void} cb - the callback will be called with an error or the path to
* the package.json file
*/
function getMainPackageJsonPathStartingAtMainModule(cb) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed anymore, because we now request pkg json + pkg json path centralized in core metrics index.js. We always provide the found path and the file to the metric.

return getMainPackageJsonPathStartingAtDirectory(null, cb);
}

/**
* Looks for path of the app's main package.json file, starting the search at the given directory. If the given
* directory is null or undefined, the search will start at path.dirname(require.main.filename).
Expand Down Expand Up @@ -187,6 +195,12 @@ function getMainPackageJsonPathStartingAtDirectory(startDirectory, cb) {
}
}

// CASE: node --require .../src/immediate.js
if (!mainModule?.filename) {
const err = new Error('Application entrypoint could not be identified.');
return process.nextTick(cb, err, null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the actual fix. @aryamohanan started with it already.
We now also forward the error.

}

startDirectory = path.dirname(mainModule.filename);
}

Expand Down Expand Up @@ -332,14 +346,14 @@ function searchInParentDir(dir, onParentDir, cb) {
const reset = () => {
parsedMainPackageJson = undefined;
mainPackageJsonPath = undefined;
attempts = 0;
};

module.exports = {
init,
isAppInstalledIntoNodeModules,
getMainPackageJsonStartingAtMainModule,
getMainPackageJsonStartingAtDirectory,
getMainPackageJsonPathStartingAtMainModule,
getMainPackageJsonPathStartingAtDirectory,
findNodeModulesFolder,
reset
Expand Down
6 changes: 3 additions & 3 deletions packages/serverless-collector/src/metrics/name.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ module.exports = config => {
}

return new Promise(resolve => {
instanaCore.util.applicationUnderMonitoring.getMainPackageJsonStartingAtMainModule((err, packageJson) => {
instanaCore.util.applicationUnderMonitoring.getMainPackageJsonStartingAtMainModule((err, packageJsonObj) => {
if (err) {
logger.debug(`Failed to determine main package.json. ${err?.message} ${err?.stack}`);
return resolve();
}

logger.debug(`Found main package.json: ${packageJson.name}`);
resolve(packageJson.name);
logger.debug(`Found main package.json: ${packageJsonObj.file.name}`);
resolve(packageJsonObj.file.name);
});
});
};
65 changes: 27 additions & 38 deletions packages/shared-metrics/src/dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,48 +36,37 @@ const preliminaryPayload = {};
// @ts-ignore: Cannot redeclare exported variable 'currentPayload'
exports.currentPayload = {};

exports.MAX_ATTEMPTS = 20;
// @ts-ignore
exports.activate = function activate(config, packageJsonObj) {
const started = Date.now();

const DELAY = 1000;
let attempts = 0;
if (!packageJsonObj || !packageJsonObj.file) {
util.applicationUnderMonitoring.findNodeModulesFolder((errNodeModules, nodeModulesFolder) => {
if (errNodeModules) {
return logger.warn(
`Failed to determine node_modules folder. Reason: ${errNodeModules?.message}, ${errNodeModules?.stack}`
);
} else if (!nodeModulesFolder) {
return logger.warn(
'Neither the package.json file nor the node_modules folder could be found. Stopping dependency analysis.'
);
}

exports.activate = function activate() {
attempts++;
addAllDependencies(path.join(nodeModulesFolder), started, null);
});

const started = Date.now();
util.applicationUnderMonitoring.getMainPackageJsonPathStartingAtMainModule((err, mainPackageJsonPath) => {
if (err) {
return logger.warn(`Failed to determine main package.json. Reason: ${err?.message}, ${err?.stack}`);
} else if (!mainPackageJsonPath && attempts < exports.MAX_ATTEMPTS) {
logger.debug(`Main package.json could not be found at ${mainPackageJsonPath}. Will try again later.`);
setTimeout(exports.activate, DELAY).unref();
return;
} else if (!mainPackageJsonPath) {
logger.info(
`Main package.json could not be found after ${attempts} retries. Looking for node_modules folder now.`
);
util.applicationUnderMonitoring.findNodeModulesFolder((errNodeModules, nodeModulesFolder) => {
if (errNodeModules) {
return logger.warn(`Failed to determine node_modules folder. Reason: ${err?.message}, ${err?.stack}`);
} else if (!nodeModulesFolder) {
return logger.warn(
'Neither the package.json file nor the node_modules folder could be found. Stopping dependency analysis.'
);
}

addAllDependencies(path.join(nodeModulesFolder), started, null);
});
return;
}
return;
}

let dependencyDir;
if (util.applicationUnderMonitoring.isAppInstalledIntoNodeModules()) {
dependencyDir = path.join(path.dirname(mainPackageJsonPath), '..', '..', 'node_modules');
} else {
dependencyDir = path.join(path.dirname(mainPackageJsonPath), 'node_modules');
}
addAllDependencies(dependencyDir, started, mainPackageJsonPath);
});
let dependencyDir;

if (util.applicationUnderMonitoring.isAppInstalledIntoNodeModules()) {
dependencyDir = path.join(path.dirname(packageJsonObj.path), '..', '..', 'node_modules');
} else {
dependencyDir = path.join(path.dirname(packageJsonObj.path), 'node_modules');
}

addAllDependencies(dependencyDir, started, packageJsonObj.path);
};

/**
Expand Down
Loading