UNPKG

@instana/collector

Version:

The Instana Node.js metrics and trace data collector

258 lines (231 loc) 9.92 kB
#!/usr/bin/env node /* * (c) Copyright IBM Corp. 2021 * (c) Copyright Instana Inc. and contributors 2020 */ /* eslint-disable no-console, max-len */ 'use strict'; // The purpose of this executable is to statically instrument a _global_ installation of Apigee's edgemicro/Microgateway // package (https://www.npmjs.com/package/edgemicro). In its default installation scenario (that is advertised in the // basic tutorials, for example), the edgemicro npm module is installed globally via npm install -g edgemicro. To start // the edgemicro Node.js process, the main executable of this globally installed package is used, like this: // edgemicro start -o $org -e $env -k $key -s $secret // // Thus, there is no user controlled code when that Node.js process is starting up, and in turn our usual installation // documented at https://www.ibm.com/docs/en/instana-observability/current?topic=nodejs-collector-installation#installing-the-collector can not be performed (in particular, there // is no user controlled code to put the require('@instana/collector')(); into. // // The purpose of this executable is to fill this gap. It needs to be called after the edgemicro package has been // installed. The package @instana/collector also needs to be installed. This executable will statically instrument the // edgemicro main CLI source file and put the require('@instana/collector')(); into the right place. Naturally, this // needs to be repeated after an update/reinstallation of the edgemicro package. // // This executable is not needed for installation scenarios where user code is started first that then requires for // example microgateway-core (https://github.com/apigee/microgateway-core). // // Why don't we simply add our require-and-init via an edgemicro plug-in? Because the plug-in code is evaluated too // late, after the workers have been already started. Thus, the instrumentation in // core/src/tracing/process/edgemicro.js and core/src/tracing/process/childProcess isn't active when workers are // started. The plug-in code would also be evaluated in each worker process, but again, too late, that is tracing would // only work partially. const childProcess = require('child_process'); const fs = require('fs'); const path = require('path'); const selfPath = require('./selfPath'); const edgemicroCliMain = 'cli/edgemicro'; /** * @param {string | ((err?: Error) => *) | undefined} edgemicroPath * @param {string | ((err?: Error) => *) | undefined} collectorPath * @param {(err?: Error) => * | undefined} callback * @returns */ function instrumentEdgemicroCli(edgemicroPath, collectorPath, callback) { if (typeof edgemicroPath === 'function') { callback = edgemicroPath; edgemicroPath = undefined; collectorPath = undefined; } if (typeof collectorPath === 'function') { callback = collectorPath; collectorPath = undefined; } if (!callback) { throw new Error('Mandatory argument callback is missing.'); } if (typeof callback !== 'function') { throw new Error(`The callback argument is not a function but of type ${typeof callback}.`); } if (!edgemicroPath) { console.log('- Path to edgemicro has not been provided, I will try to figure it out now.'); const globalNodeModules = childProcess.execSync('npm root -g').toString().trim(); console.log(' * Global node_modules directory:', globalNodeModules); edgemicroPath = path.join(globalNodeModules, 'edgemicro'); if (!fs.existsSync(edgemicroPath)) { return callback( new Error( `It seems there is no edgemicro installation at ${edgemicroPath}. You can also provide the path to your edgemicro installation explicitly as a command line argument.` ) ); } console.log(' * Global edgemicro installation should be in:', edgemicroPath); console.log('- Path to edgemicro has not been provided, I will assume it is:', edgemicroPath); } if (typeof edgemicroPath !== 'string') { return callback(new Error(`The path to edgemicro needs to be a string but was of type ${typeof edgemicroPath}`)); } if (!collectorPath) { collectorPath = selfPath.collectorPath; console.log('- Path to @instana/collector has not been provided, I will assume it is:', collectorPath); } if (typeof collectorPath !== 'string') { return callback( new Error(`The path to @instana/collector needs to be a string but was of type ${typeof collectorPath}.`) ); } console.log('- Provided arguments:'); console.log(' * Path to the edgemicro package:', edgemicroPath); if (!path.isAbsolute(edgemicroPath)) { edgemicroPath = path.resolve(edgemicroPath); console.log(' * resolved absolute path for edgemicro package:', edgemicroPath); } console.log(' * Path to the @instana/collector:', collectorPath); if (!path.isAbsolute(collectorPath)) { collectorPath = path.resolve(collectorPath); console.log(' * resolved absolute path for @instana/collector:', collectorPath); } console.log(`- Checking if @instana/collector exists at ${collectorPath}.`); fs.access(collectorPath, fs.constants.F_OK, fsAccessError => { if (fsAccessError) { console.log(fsAccessError); return callback(fsAccessError); } try { const collectorPackageJson = require(path.join(/** @type {string} */ (collectorPath), 'package.json')); if (collectorPackageJson.name !== '@instana/collector') { return callback( new Error( `The provided path for @instana/collector does not seem to be valid, expected the package name to be @instana/collector, instead the name "${collectorPackageJson.name}" has been found.` ) ); } } catch (packageJsonError) { return callback( new Error( `The provided path for @instana/collector does not seem to be valid, there is no package.json at the expected location or it cannot be parsed: ${packageJsonError.stack}` ) ); } const edgemicroCliMainFullPath = path.resolve(/** @type {string} */ (edgemicroPath), edgemicroCliMain); console.log('- Will instrument the following file:', edgemicroCliMainFullPath); createBackupAndInstrument(edgemicroCliMainFullPath, /** @type {string} */ (collectorPath), callback); }); } module.exports = instrumentEdgemicroCli; /** * @param {string} edgemicroCliMainFullPath * @param {string} collectorPath * @param {(err?: Error) => *} callback */ function createBackupAndInstrument(edgemicroCliMainFullPath, collectorPath, callback) { const backupFullPath = `${edgemicroCliMainFullPath}.backup`; console.log('- Creating a backup at:', backupFullPath); copyFile(edgemicroCliMainFullPath, backupFullPath, copyErr => { if (copyErr) { console.error(copyErr); return callback(copyErr); } instrument(edgemicroCliMainFullPath, collectorPath, callback); }); } /** * @param {string} source * @param {string} target * @param {(err?: Error) => *} copyCallback */ function copyFile(source, target, copyCallback) { let callbackHasBeenCalled = false; const readStream = fs.createReadStream(source); const writeBackupStream = fs.createWriteStream(target); readStream.on('error', err => { if (!callbackHasBeenCalled) { callbackHasBeenCalled = true; copyCallback(err); } }); writeBackupStream.on('error', err => { if (!callbackHasBeenCalled) { callbackHasBeenCalled = true; copyCallback(err); } }); writeBackupStream.on('finish', () => { if (!callbackHasBeenCalled) { callbackHasBeenCalled = true; copyCallback(); } }); readStream.pipe(writeBackupStream); } /** * @param {string} fileToBeInstrumented * @param {string} collectorPath * @param {(err?: Error) => *} callback */ function instrument(fileToBeInstrumented, collectorPath, callback) { console.log('- Reading:', fileToBeInstrumented); fs.readFile(fileToBeInstrumented, 'utf8', (readErr, content) => { if (readErr) { console.error(readErr); return callback(readErr); } let result; const match = /\nrequire[^\n]*collector[^\n]*\n/.exec(content); if (match) { result = `${content.substring(0, match.index + 1)}require('${collectorPath}')();\n${content.substring( match.index + match[0].length )}`; } else { result = content.replace(/\n'use strict';\n/, `\n'use strict';\nrequire('${collectorPath}')();\n`); } console.log('- Writing:', fileToBeInstrumented); fs.writeFile(fileToBeInstrumented, result, 'utf8', writeErr => { if (writeErr) { console.error(writeErr); return callback(writeErr); } callback(); }); }); } if (require.main === module) { // The file is running as a script, kick off the instrumentation directly. module.exports(process.argv[2], process.argv[3], err => { if (err) { console.error('Failed to instrument the edgemicro module', err); process.exit(1); } console.log( '- Done: The edgemicro module has been statically instrumented for Instana tracing and metrics collection.' ); }); } else { if ( // @ts-ignore - TS doesn't recognize the internal properties of module module.parent && // @ts-ignore typeof module.parent.id === 'string' && // @ts-ignore module.parent.id.indexOf('instrument_edgemicro_cli_test') >= 0 ) { // skip printing warnings in tests // @ts-ignore - A 'return' statement can only be used within a function body return; } // Not running as a script, wait for client code to trigger the instrumentation. console.warn( `The file ${path.join( __dirname, 'instrument-edgemicro-cli.js' )} has been required by another module instead of being run directly as a script. You need to call the exported function yourself to start the instrumentation in this scenario.` ); }