UNPKG

@instana/shared-metrics

Version:

Internal metrics plug-in package for Node.js monitoring with Instana

278 lines (248 loc) 11.6 kB
/* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2021 */ 'use strict'; 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 };