@instana/shared-metrics
Version:
Internal metrics plug-in package for Node.js monitoring with Instana
278 lines (248 loc) • 11.6 kB
JavaScript
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2021
*/
;
const assert = require('assert');
const { uninstrumentedFs: fs } = require('@instana/core');
const path = require('path');
const CountDownLatch = require('./CountDownLatch');
/** @type {import('@instana/core/src/core').GenericLogger} */
let logger;
/**
* @param {import('@instana/core/src/config').InstanaConfig} config
*/
const init = config => {
logger = config.logger;
};
class DependencyDistanceCalculator {
/**
* Calculates the distance for all dependencies, starting at the given package.json file. Direct dependencies listed
* in the passed package.json have distance 1. Dependencies of those dependencies have distance 2, and so on. This is
* calculated by parsing package.json files recursively, traversing the tree of dependencies.
*
* @param {string} packageJsonPath the path to the package.json file to examine initially
* @param {(distances: Object<string, any>) => void} callback
*/
calculateDistancesFrom(packageJsonPath, callback) {
this.started = Date.now();
assert.strictEqual(typeof packageJsonPath, 'string');
assert.strictEqual(typeof callback, 'function');
/** @type {Object.<string, any>} */
this.distancesFromRoot = {};
this.globalCountDownLatchAllPackages = new CountDownLatch(0);
this.globalCountDownLatchAllPackages.once('done', () => {
logger.debug(`Calculation of dependency distances took ${Date.now() - this.started} ms.`);
callback(this.distancesFromRoot);
});
this._calculateDistances(packageJsonPath, 1);
}
/**
* Calculates the distances for the dependencies in the given package.json file. Direct dependencies of the
* application have distance 1. Dependencies of those dependencies have distance 2, and so on. This is calculated by
* parsing package.json files recursively, traversing the tree of dependencies.
*
* @param {string} packageJsonPath The path to the package.json file to examine
* @param {number} distance The distance from the application along the tree of dependencies
*/
_calculateDistances(packageJsonPath, distance) {
if (distance > module.exports.MAX_DEPTH) {
// Do not descend deeper than maxDepth nesting levels.
return;
}
if (typeof packageJsonPath !== 'string') {
return;
}
// For each package.json that we find in the dependency tree, we initially increase the global count down latch
// by 3, that is, one for each type of dependencies (normal, optional, peer). Once we have in turn queued all
// dependencies found in this package.json for a particular dependency type, we decrement the global count down
// latch by one. When this has happend for all three types of dependencies, the net change for the global latch will
// be zero, but sub dependencies will already have been incremented the global count down latch.
this.globalCountDownLatchAllPackages.countUp(3);
// Read the associated package.json and parse it.
try {
fs.readFile(packageJsonPath, { encoding: 'utf8' }, (err, contents) => {
if (err) {
logger.debug(
`Failed to calculate transitive distances for some dependencies, could not read package.json file at
${packageJsonPath}: ${err?.message}.`
);
// If we cannot parse the package.json or if it does not exist, we need to decrement by 3 immediately because
// we increment the latch by 3 for each node (see above).
this.globalCountDownLatchAllPackages.countDown(3);
return;
}
let parsedPackageJson;
try {
parsedPackageJson = JSON.parse(contents);
} catch (parseErr) {
logger.debug(
`Failed to calculate transitive distances for some dependencies, could not parse package.json file at
${packageJsonPath}: ${parseErr?.message}.`
);
this.globalCountDownLatchAllPackages.countDown(3);
return;
}
// Each call to _calculateDistancesForOneType is guaranteed to decrease the global count down latch by exactly
// one, to offset the increment of 3 that we did for this node in the dependency tree initially.
this._calculateDistancesForOneType(parsedPackageJson.dependencies, distance);
this._calculateDistancesForOneType(parsedPackageJson.peerDependencies, distance);
this._calculateDistancesForOneType(parsedPackageJson.optionalDependencies, distance);
});
} catch (fsReadFileErr) {
// This catch-block is for synchronous errors from fs.readFile, which can also happen in addition to the callback
// being called with an error.
logger.debug(
`Failed to calculate transitive distances for some dependencies, synchronous error from fs.readFile for
${packageJsonPath}: ${fsReadFileErr?.message}`
);
this.globalCountDownLatchAllPackages.countDown(3);
}
}
/**
* Iterates over the given set of dependencies to calculate their distances. The set of dependencies will what is
* defined in a package.json file for one particular type of dependencys (normal, optional, or peer).
*
* @param {Array<string>} dependencies The dependencies to analyze
* @param {number} distance How far the dependencies are from the root package
*/
_calculateDistancesForOneType(dependencies, distance) {
if (!dependencies) {
this.globalCountDownLatchAllPackages.countDown();
return;
}
const keys = Object.keys(dependencies);
if (keys.length === 0) {
this.globalCountDownLatchAllPackages.countDown();
return;
}
// This local latch is initialized with the number of dependencies of the current package.json file for the
// particular dependency type (normal dependencies, optional ones, peer dependencies) we are analyzing at the
// moment. Once all sub dependencies for the current package and type have been either
//
// a) scheduled for analysis (and have in turn incremented the global count down latch), or
// b) have been found to not need further analysis,
//
// we consider the current node to be done and reduce the global counter/ accordingly.
const localCountDownLatchForThisNode = new CountDownLatch(keys.length);
localCountDownLatchForThisNode.once('done', () => {
this.globalCountDownLatchAllPackages.countDown();
});
for (let i = 0; i < keys.length; i++) {
const dependency = keys[i];
if (this.distancesFromRoot[dependency]) {
// We have seen this package before. Do not analyze this package again.
this.distancesFromRoot[dependency] = Math.min(distance, this.distancesFromRoot[dependency]);
localCountDownLatchForThisNode.countDown();
continue;
}
// We have not seen this package yet, store the distance for it.
this.distancesFromRoot[dependency] = distance;
// Queue this dependency up for further analysis. The local latch is only decremented after we have incremented
// the/ global latch for this dependency. This makes sure we do not stop the analysis too early.
this._handleTransitiveDependency(dependency, distance, localCountDownLatchForThisNode);
}
}
/**
* Handles a single dependency found in a package.json file.
*
* @param {string} dependency the name of the dependency to analyze
* @param {number} distance how far this dependency is from the root package
* @param {import('./CountDownLatch')} localCountDownLatchForThisNode
*/
_handleTransitiveDependency(dependency, distance, localCountDownLatchForThisNode) {
let mainModulePath;
try {
mainModulePath = require.resolve(dependency);
} catch (requireResolveErr) {
// ignore
logger.trace(
`Ignoring failure to resolve the path to dependency ${dependency} for dependency distance calculation.`
);
}
if (!mainModulePath) {
// Could not find the package.json for this dependency so we cannot analyze it further, which means we are done
// with it.
localCountDownLatchForThisNode.countDown();
logger.trace(`Ignoring ${dependency} for dependency distance calculation, could not find main module path.`);
return;
}
findPackageJsonFor(path.dirname(mainModulePath), (err, packageJsonPath) => {
if (err) {
localCountDownLatchForThisNode.countDown();
logger.debug(
`Ignoring failure to find the package.json file for dependency ${dependency} for dependency distance ` +
`calculation. ${err?.message} ${err?.stack}`
);
return;
}
if (typeof packageJsonPath !== 'string') {
localCountDownLatchForThisNode.countDown();
logger.debug(
`Ignoring failure to find the package.json file for dependency ${dependency} for dependency distance ` +
`calculation (package.json path is ${packageJsonPath}/${typeof packageJsonPath}).`
);
return;
}
// Recurse one level deeper and queue the next package.json path for analysis.
this._calculateDistances(packageJsonPath, distance + 1);
// After the _calculateDistances call we have "handled" the package associated to packageJsonPath, that is, we
// have (synchronously) incremented the global latch for it. That is, unless we have hit the depth limit, but even
// then we consider that package to have been handled). Thus, we can now decrement the local latch for it.
localCountDownLatchForThisNode.countDown();
});
}
}
/**
* Finds the package.json file for a given directory, by starting in the given directory and then travelling the
* directory tree upwards until a package.json file is found.
*
* @param {string} dir
* @param {(err: Error, packageJsonPath: string) => void} callback
*/
function findPackageJsonFor(dir, callback) {
const pathToCheck = path.join(dir, 'package.json');
try {
fs.stat(pathToCheck, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return searchInParentDir(dir, findPackageJsonFor, callback);
} else {
return process.nextTick(callback, err, null);
}
}
if (stats.isFile()) {
return process.nextTick(callback, null, pathToCheck);
} else {
return searchInParentDir(dir, findPackageJsonFor, callback);
}
});
} catch (fsStatErr) {
// This catch-block is for synchronous errors from fs.stat, which can also happen in addition to the callback being
// called with an error. The error will be logged in _handleTransitiveDependency.
return process.nextTick(callback, fsStatErr, null);
}
}
/**
* Goes to the parent directory of the given dir and executes the function onParentDir on it.
*
* @param {string} dir
* @param {(parentDir: string, callback: Function) => void} onParentDir
* @param {(err: Error, packageJsonPath: string) => void} callback
*/
function searchInParentDir(dir, onParentDir, callback) {
const parentDir = path.resolve(dir, '..');
if (dir === parentDir) {
// We have arrived at the root of the file system hierarchy.
// findPackageJsonFor would have called callback asynchronously,
// so we use process.nextTick here to make all paths async.
return process.nextTick(callback, null, null);
}
return onParentDir(parentDir, callback);
}
module.exports = {
init,
DependencyDistanceCalculator,
MAX_DEPTH: 15,
__moduleRefExportedForTest: module
};