@opentelemetry/instrumentation
Version:
Base class for node which OpenTelemetry instrumentation modules extend
282 lines • 11.7 kB
JavaScript
"use strict";
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InstrumentationBase = void 0;
const path = require("path");
const util_1 = require("util");
const semver_1 = require("../../semver");
const shimmer_1 = require("shimmer");
const instrumentation_1 = require("../../instrumentation");
const RequireInTheMiddleSingleton_1 = require("./RequireInTheMiddleSingleton");
const import_in_the_middle_1 = require("import-in-the-middle");
const api_1 = require("@opentelemetry/api");
const require_in_the_middle_1 = require("require-in-the-middle");
const fs_1 = require("fs");
const utils_1 = require("../../utils");
/**
* Base abstract class for instrumenting node plugins
*/
class InstrumentationBase extends instrumentation_1.InstrumentationAbstract {
_modules;
_hooks = [];
_requireInTheMiddleSingleton = RequireInTheMiddleSingleton_1.RequireInTheMiddleSingleton.getInstance();
_enabled = false;
constructor(instrumentationName, instrumentationVersion, config) {
super(instrumentationName, instrumentationVersion, config);
let modules = this.init();
if (modules && !Array.isArray(modules)) {
modules = [modules];
}
this._modules = modules || [];
if (this._config.enabled) {
this.enable();
}
}
_wrap = (moduleExports, name, wrapper) => {
if ((0, utils_1.isWrapped)(moduleExports[name])) {
this._unwrap(moduleExports, name);
}
if (!util_1.types.isProxy(moduleExports)) {
return (0, shimmer_1.wrap)(moduleExports, name, wrapper);
}
else {
const wrapped = (0, shimmer_1.wrap)(Object.assign({}, moduleExports), name, wrapper);
Object.defineProperty(moduleExports, name, {
value: wrapped,
});
return wrapped;
}
};
_unwrap = (moduleExports, name) => {
if (!util_1.types.isProxy(moduleExports)) {
return (0, shimmer_1.unwrap)(moduleExports, name);
}
else {
return Object.defineProperty(moduleExports, name, {
value: moduleExports[name],
});
}
};
_massWrap = (moduleExportsArray, names, wrapper) => {
if (!moduleExportsArray) {
api_1.diag.error('must provide one or more modules to patch');
return;
}
else if (!Array.isArray(moduleExportsArray)) {
moduleExportsArray = [moduleExportsArray];
}
if (!(names && Array.isArray(names))) {
api_1.diag.error('must provide one or more functions to wrap on modules');
return;
}
moduleExportsArray.forEach(moduleExports => {
names.forEach(name => {
this._wrap(moduleExports, name, wrapper);
});
});
};
_massUnwrap = (moduleExportsArray, names) => {
if (!moduleExportsArray) {
api_1.diag.error('must provide one or more modules to patch');
return;
}
else if (!Array.isArray(moduleExportsArray)) {
moduleExportsArray = [moduleExportsArray];
}
if (!(names && Array.isArray(names))) {
api_1.diag.error('must provide one or more functions to wrap on modules');
return;
}
moduleExportsArray.forEach(moduleExports => {
names.forEach(name => {
this._unwrap(moduleExports, name);
});
});
};
_warnOnPreloadedModules() {
this._modules.forEach((module) => {
const { name } = module;
try {
const resolvedModule = require.resolve(name);
if (require.cache[resolvedModule]) {
// Module is already cached, which means the instrumentation hook might not work
this._diag.warn(`Module ${name} has been loaded before ${this.instrumentationName} so it might not work, please initialize it before requiring ${name}`);
}
}
catch {
// Module isn't available, we can simply skip
}
});
}
_extractPackageVersion(baseDir) {
try {
const json = (0, fs_1.readFileSync)(path.join(baseDir, 'package.json'), {
encoding: 'utf8',
});
const version = JSON.parse(json).version;
return typeof version === 'string' ? version : undefined;
}
catch (error) {
api_1.diag.warn('Failed extracting version', baseDir);
}
return undefined;
}
_onRequire(module, exports, name, baseDir) {
if (!baseDir) {
if (typeof module.patch === 'function') {
module.moduleExports = exports;
if (this._enabled) {
this._diag.debug('Applying instrumentation patch for nodejs core module on require hook', {
module: module.name,
});
return module.patch(exports);
}
}
return exports;
}
const version = this._extractPackageVersion(baseDir);
module.moduleVersion = version;
if (module.name === name) {
// main module
if (isSupported(module.supportedVersions, version, module.includePrerelease)) {
if (typeof module.patch === 'function') {
module.moduleExports = exports;
if (this._enabled) {
this._diag.debug('Applying instrumentation patch for module on require hook', {
module: module.name,
version: module.moduleVersion,
baseDir,
});
return module.patch(exports, module.moduleVersion);
}
}
}
return exports;
}
// internal file
const files = module.files ?? [];
const normalizedName = path.normalize(name);
const supportedFileInstrumentations = files
.filter(f => f.name === normalizedName)
.filter(f => isSupported(f.supportedVersions, version, module.includePrerelease));
return supportedFileInstrumentations.reduce((patchedExports, file) => {
file.moduleExports = patchedExports;
if (this._enabled) {
this._diag.debug('Applying instrumentation patch for nodejs module file on require hook', {
module: module.name,
version: module.moduleVersion,
fileName: file.name,
baseDir,
});
// patch signature is not typed, so we cast it assuming it's correct
return file.patch(patchedExports, module.moduleVersion);
}
return patchedExports;
}, exports);
}
enable() {
if (this._enabled) {
return;
}
this._enabled = true;
// already hooked, just call patch again
if (this._hooks.length > 0) {
for (const module of this._modules) {
if (typeof module.patch === 'function' && module.moduleExports) {
this._diag.debug('Applying instrumentation patch for nodejs module on instrumentation enabled', {
module: module.name,
version: module.moduleVersion,
});
module.patch(module.moduleExports, module.moduleVersion);
}
for (const file of module.files) {
if (file.moduleExports) {
this._diag.debug('Applying instrumentation patch for nodejs module file on instrumentation enabled', {
module: module.name,
version: module.moduleVersion,
fileName: file.name,
});
file.patch(file.moduleExports, module.moduleVersion);
}
}
}
return;
}
this._warnOnPreloadedModules();
for (const module of this._modules) {
const hookFn = (exports, name, baseDir) => {
if (!baseDir && path.isAbsolute(name)) {
const parsedPath = path.parse(name);
name = parsedPath.name;
baseDir = parsedPath.dir;
}
return this._onRequire(module, exports, name, baseDir);
};
const onRequire = (exports, name, baseDir) => {
return this._onRequire(module, exports, name, baseDir);
};
// `RequireInTheMiddleSingleton` does not support absolute paths.
// For an absolute paths, we must create a separate instance of the
// require-in-the-middle `Hook`.
const hook = path.isAbsolute(module.name)
? new require_in_the_middle_1.Hook([module.name], { internals: true }, onRequire)
: this._requireInTheMiddleSingleton.register(module.name, onRequire);
this._hooks.push(hook);
const esmHook = new import_in_the_middle_1.Hook([module.name], { internals: false }, hookFn);
this._hooks.push(esmHook);
}
}
disable() {
if (!this._enabled) {
return;
}
this._enabled = false;
for (const module of this._modules) {
if (typeof module.unpatch === 'function' && module.moduleExports) {
this._diag.debug('Removing instrumentation patch for nodejs module on instrumentation disabled', {
module: module.name,
version: module.moduleVersion,
});
module.unpatch(module.moduleExports, module.moduleVersion);
}
for (const file of module.files) {
if (file.moduleExports) {
this._diag.debug('Removing instrumentation patch for nodejs module file on instrumentation disabled', {
module: module.name,
version: module.moduleVersion,
fileName: file.name,
});
file.unpatch(file.moduleExports, module.moduleVersion);
}
}
}
}
isEnabled() {
return this._enabled;
}
}
exports.InstrumentationBase = InstrumentationBase;
function isSupported(supportedVersions, version, includePrerelease) {
if (typeof version === 'undefined') {
// If we don't have the version, accept the wildcard case only
return supportedVersions.includes('*');
}
return supportedVersions.some(supportedVersion => {
return (0, semver_1.satisfies)(version, supportedVersion, { includePrerelease });
});
}
//# sourceMappingURL=instrumentation.js.map