-
Notifications
You must be signed in to change notification settings - Fork 40
Expand file tree
/
Copy pathapplicationUnderMonitoring.js
More file actions
360 lines (313 loc) · 12.6 KB
/
applicationUnderMonitoring.js
File metadata and controls
360 lines (313 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
/*
* (c) Copyright IBM Corp. 2021
* (c) Copyright Instana Inc. and contributors 2015
*/
'use strict';
const fs = require('../uninstrumentedFs');
const path = require('path');
const isESMApp = require('./esm').isESMApp;
/** @type {import('../core').GenericLogger} */
let logger;
// Cache determined main package json as these will be referenced often
// and identification of these values is expensive.
/** @type {Object.<string, *>} */
let parsedMainPackageJson;
/** @type {string} */
let mainPackageJsonPath;
/** @type {Array.<string>} */
let nodeModulesPath;
let appInstalledIntoNodeModules = false;
/** @type {string} */
let packageJsonPath;
/**
* @param {import('../config').InstanaConfig} config
*/
function init(config) {
logger = config.logger;
packageJsonPath = config.packageJsonPath;
}
function isAppInstalledIntoNodeModules() {
return appInstalledIntoNodeModules;
}
const MAX_ATTEMPTS = 5;
const DELAY = 1500;
let attempts = 0;
/**
* 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).
*
* In case the search is successful, the result will be cached for consecutive invocations.
*
* @param {(err: Error, parsedMainPackageJson: Object.<string, *>) => void } cb - the callback will be called with an
* error or the parsed package.json file as a JS object.
*/
function getMainPackageJsonStartingAtMainModule(cb) {
// NOTE: we already cached the package.json
if (parsedMainPackageJson !== undefined) {
return cb(null, { file: parsedMainPackageJson, path: mainPackageJsonPath });
}
// CASE: customer provided custom package.json path, let's try loading it
if (packageJsonPath) {
return readFile(packageJsonPath, 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);
});
}
/**
* Looks for the app's main package.json file, parses it and returns the parsed content. If the given directory is null
* or undefined, the search will start at path.dirname(require.main.filename).
*
* In case the search is successful, the result will be cached for consecutive invocations.
*
* @param {string} startDirectory - the directory in which to start searching.
* @param {(err: Error, parsedMainPackageJson: Object.<string, *>) => void } cb - the callback will be called with an
* error or the parsed package.json file as a JS object.
*/
function getMainPackageJsonStartingAtDirectory(startDirectory, cb) {
// NOTE: we already cached the package.json. We need the caching here too, because
// e.g. `npmPackageVersion` uses this function directly. It's duplicated code, but its acceptable.
if (parsedMainPackageJson !== undefined) {
return cb(null, parsedMainPackageJson);
}
getMainPackageJsonPathStartingAtDirectory(startDirectory, (err, foundPackageJsonPath) => {
if (err) {
// fs.readFile would have called cb asynchronously later, so we use process.nextTick here to make all paths async.
return process.nextTick(cb, err, null);
}
if (foundPackageJsonPath == null) {
// fs.readFile would have called cb asynchronously later, so we use process.nextTick here to make all paths async.
return process.nextTick(cb);
}
readFile(foundPackageJsonPath, cb);
});
}
/**
*
* @param {string} filePath
* @param {function} cb
*/
function readFile(filePath, cb) {
fs.readFile(filePath, { encoding: 'utf8' }, (readFileErr, contents) => {
if (readFileErr) {
return cb(readFileErr, null);
}
try {
parsedMainPackageJson = JSON.parse(contents);
} catch (e) {
logger.warn(`Package.json file ${packageJsonPath} cannot be parsed: ${e?.message} ${e?.stack}`);
return cb(e, null);
}
return cb(null, { file: parsedMainPackageJson, path: filePath });
});
}
/**
* 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).
*
* In case the search is successful, the result will be cached for consecutive invocations.
*
* @param {string} startDirectory - the directory in which to start searching.
* @param {(err: Error, packageJsonPath: string) => void} cb - the callback will be called with an error or the path to
* the package.json file
*/
function getMainPackageJsonPathStartingAtDirectory(startDirectory, cb) {
if (mainPackageJsonPath !== undefined) {
// searchForPackageJsonInDirectoryTreeUpwards would have called cb asynchronously later,
// so we use process.nextTick here to make all paths async.
return process.nextTick(cb, null, mainPackageJsonPath);
}
if (!startDirectory) {
// No explicit starting directory for searching for the main package.json has been provided, use the Node.js
// "require.main" module as the starting point.
// NOTE: "require.main" is undefined when the Instana collector is required inside a
// preloaded module using "--require". "process.mainModule" is not, but it is deprecated and should
// no longer been used.
let mainModule = require.main;
if (!mainModule) {
// This happens
// a) when the Node CLI is evaluating an expression, or
// b) when the REPL is used
// c) when we have been pre-required with the --require/-r command line flag.
// d) when --experimental-loader is used for ESM apps.
//
// In particular for case (c) we can try again later and wait for the main module to be loaded.
// But usually that is not necessary because the initialisation of the collector takes longer than
// Node.js having not loaded the main module. Still, it is "ok" to keep the retry mechanismn inside
// the individual metrics (e.g. name.js) just for safety.
//
// But when the application is using ES modules and they require the Instana collector
// with "--require file.cjs", neither `require.main` nor the deprecated `process.mainModule`
// will have a value, because ES modules use `import.meta` and soon `import.main`.
// See https://github.com/nodejs/modules/issues/274
// eslint-disable-next-line max-len
// See https://github.com/nodejs/node/blob/472edc775d683aed2d9ed39ca7cf381f3e7e3ce2/lib/internal/modules/run_main.js#L79
// Node.js is using `process.argv[1]` internally as main file path.
// Check whether a module was preloaded and use process.argv[1] as filename.
if (
// @ts-ignore
(process._preload_modules && process._preload_modules.length > 0) ||
isESMApp()
) {
// @ts-ignore
mainModule = {
filename: process.argv[1]
};
} else {
return process.nextTick(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);
}
startDirectory = path.dirname(mainModule.filename);
}
searchForPackageJsonInDirectoryTreeUpwards(startDirectory, (err, main) => {
if (err) {
return cb(err, null);
}
mainPackageJsonPath = main;
return cb(null, mainPackageJsonPath);
});
}
/**
* @param {string} dir
* @param {(err: Error, main: *) => void} cb
*/
function searchForPackageJsonInDirectoryTreeUpwards(dir, cb) {
const pathToCheck = path.join(dir, 'package.json');
fs.stat(pathToCheck, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return searchInParentDir(dir, searchForPackageJsonInDirectoryTreeUpwards, cb);
} else {
// searchInParentDir would have called cb asynchronously,
// so we use process.nextTick here to make all paths async.
return process.nextTick(cb, err, null);
}
}
appInstalledIntoNodeModules = dir.indexOf('node_modules') >= 0;
if (appInstalledIntoNodeModules) {
// Some users do not deploy their app by cloning/copying the app's sources to the target system and installing its
// dependencies via npm/yarn there. Instead, they publish the whole app into an npm-compatible registry and use
// npm install $appName on the target system to deploy the app including its dependencies. In this scenario, we
// need to skip the check for an accompanying node_modules folder (see below). We can recognize this pattern
// (heuristically) by the fact that the segment 'node_modules' already appears in the path to the main module.
return process.nextTick(cb, null, pathToCheck);
}
// If the package.json file actually exists, we also need to make sure that there is a node_modules directory
// located next to it. This way we can be relatively certain that we did not encounter a component package.json
// (as used by React for example). It is highly unlikely that the application has no dependencies, because
// @instana/core is a dependency itself.
if (stats.isFile()) {
const potentialNodeModulesDir = path.join(dir, 'node_modules');
fs.stat(potentialNodeModulesDir, (statErr, potentialNodeModulesDirStats) => {
if (statErr) {
if (statErr.code === 'ENOENT') {
return searchInParentDir(dir, searchForPackageJsonInDirectoryTreeUpwards, cb);
}
// searchInParentDir would have called cb asynchronously,
// so we use process.nextTick here to make all paths async.
return process.nextTick(cb, statErr, null);
}
if (potentialNodeModulesDirStats.isDirectory()) {
// We have found a package.json which has dependencies located next to it. We assume that this is the
// package.json file we are looking for.
// Also, searchInParentDir would have called cb asynchronously,
// so we use process.nextTick here to make all paths async.
return process.nextTick(cb, null, pathToCheck);
} else {
return searchInParentDir(dir, searchForPackageJsonInDirectoryTreeUpwards, cb);
}
});
} else {
return searchInParentDir(dir, searchForPackageJsonInDirectoryTreeUpwards, cb);
}
});
}
/**
* @param {(errNodeModules: *, nodeModulesFolder: *) => *} cb
*/
function findNodeModulesFolder(cb) {
if (nodeModulesPath !== undefined) {
return process.nextTick(cb, null, nodeModulesPath);
}
const mainModule = require.main;
if (!mainModule) {
return process.nextTick(cb);
}
const startDirectory = path.dirname(mainModule.filename);
searchForNodeModulesInDirectoryTreeUpwards(startDirectory, (err, nodeModulesPath_) => {
if (err) {
return cb(err, null);
}
nodeModulesPath = nodeModulesPath_;
return cb(null, nodeModulesPath);
});
}
/**
* @param {string} dir
* @param {(err: Error, nodeModulesPath: *) => void} cb
*/
function searchForNodeModulesInDirectoryTreeUpwards(dir, cb) {
const pathToCheck = path.join(dir, 'node_modules');
fs.stat(pathToCheck, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return searchInParentDir(dir, searchForNodeModulesInDirectoryTreeUpwards, cb);
} else {
// searchInParentDir would have called cb asynchronously,
// so we use process.nextTick here to make all paths async.
return process.nextTick(cb, err, null);
}
}
if (stats.isDirectory()) {
return process.nextTick(cb, null, pathToCheck);
} else {
return searchInParentDir(dir, searchForNodeModulesInDirectoryTreeUpwards, cb);
}
});
}
/**
* @param {string} dir
* @param {(parentDir: string, cb: Function) => void} onParentDir
* @param {Function} cb
*/
function searchInParentDir(dir, onParentDir, cb) {
const parentDir = path.resolve(dir, '..');
if (dir === parentDir) {
// We have arrived at the root of the file system hierarchy.
//
// searchForPackageJsonInDirectoryTreeUpwards would have called cb asynchronously,
// so we use process.nextTick here to make all paths async.
return process.nextTick(cb, null, null);
}
return onParentDir(parentDir, cb);
}
const reset = () => {
parsedMainPackageJson = undefined;
mainPackageJsonPath = undefined;
attempts = 0;
};
module.exports = {
init,
isAppInstalledIntoNodeModules,
getMainPackageJsonStartingAtMainModule,
getMainPackageJsonStartingAtDirectory,
getMainPackageJsonPathStartingAtDirectory,
findNodeModulesFolder,
reset
};