-
Notifications
You must be signed in to change notification settings - Fork 533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ioredis: cannot instrument an ESM-imported ioredis #1692
Comments
I can take a look. |
I've added the following to the instrumentation-ioredis/src/instrumentation.ts console.log('XXX --- instrumentation-ioredis init()');
console.log('XXX moduleExports: ', moduleExports);
console.log('XXX moduleExports.default: ', moduleExports.default); First let's reproduce with "test.mjs" (trimmed a bit for brevity):
Then let's run the equivalent CommonJS version: const {IORedisInstrumentation} = require("@opentelemetry/instrumentation-ioredis");
new IORedisInstrumentation()
require('ioredis');
setTimeout(() => 0, 1000)
The relevant difference between the two is:
The top-level "thing" from an ESM import, the thing that is passed to the For some interoperability, Node.js' ESM loader adds a https://nodejs.org/api/esm.html#commonjs-namespaces
For ioredis, the top-level export is a class object that the current OTel instrumentation is trying to use. For a CommonJS import of ioredis, that same class is also available at ...
exports = module.exports = require("./Redis").default;
var Redis_1 = require("./Redis");
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return Redis_1.default; } });
... That means we should be able to change the instrumentation-ioredis to instrument Trying that gets us further. More below. AsideThis " moduleExports[Symbol.toStringTag] === "Module" The next issueMaking the change to patch
Things work fine with CJS:
I modified "instrumentation-ioredis/build/src/instrumentation.js" to dump that
When run via CommonJS it looks like:
But when run via ESM it does not have the Here is what I believe is happening. First, ESM imports happen before other code is executed, so "test.mjs" is effectively this:
To support this behaviour,
That should be fine, except I was a little surprised by the resultant JavaScript from the If we change the definition of those to methods not using arrow-functions: diff --git a/plugins/node/opentelemetry-instrumentation-ioredis/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-ioredis/src/instrumentation.ts
index 94f8b850..50a64993 100644
--- a/plugins/node/opentelemetry-instrumentation-ioredis/src/instrumentation.ts
+++ b/plugins/node/opentelemetry-instrumentation-ioredis/src/instrumentation.ts
@@ -84,17 +84,17 @@ export class IORedisInstrumentation extends InstrumentationBase<any> {
*/
private _patchSendCommand(moduleVersion?: string) {
return (original: Function) => {
- return this.traceSendCommand(original, moduleVersion);
+ return this._traceSendCommand(original, moduleVersion);
};
}
private _patchConnection() {
return (original: Function) => {
- return this.traceConnection(original);
+ return this._traceConnection(original);
};
}
- private traceSendCommand = (original: Function, moduleVersion?: string) => {
+ private _traceSendCommand(original: Function, moduleVersion?: string) {
const instrumentation = this;
return function (this: RedisInterface, cmd?: IORedisCommand) {
if (arguments.length < 1 || typeof cmd !== 'object') {
@@ -178,9 +178,9 @@ export class IORedisInstrumentation extends InstrumentationBase<any> {
throw error;
}
};
- };
+ }
- private traceConnection = (original: Function) => {
+ private _traceConnection(original: Function) {
const instrumentation = this;
return function (this: RedisInterface) {
const config =
@@ -213,5 +213,5 @@ export class IORedisInstrumentation extends InstrumentationBase<any> {
throw error;
}
};
- };
+ }
} Then things work:
I'll work on a PR. |
Nice analysis. In special the last part with missing properties remembers me to open-telemetry/opentelemetry-js#1989 and open-telemetry/opentelemetry-js#3837 |
Thanks for the links, especially the former that links to microsoft/TypeScript#9209 (comment) |
Here is a slightly longer test case, that generates some spans to sanity check that ioredis instrumentation is working: import {IORedisInstrumentation} from "@opentelemetry/instrumentation-ioredis";
import { NodeSDK, tracing, api } from '@opentelemetry/sdk-node';
const { SimpleSpanProcessor, ConsoleSpanExporter } = tracing;
import Redis from 'ioredis';
// diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
const sdk = new NodeSDK({
serviceName: 'test-redis-instr',
spanProcessor: new SimpleSpanProcessor(new ConsoleSpanExporter()),
instrumentations: [
new IORedisInstrumentation()
]
})
sdk.start();
const tracer = api.trace.getTracer('default');
const redis = new Redis();
await tracer.startActiveSpan('manual-span', async (span) => {
redis.set('foo', 'bar');
const val = await redis.get('foo');
console.log('foo: %s', val);
span.end()
})
await redis.quit(); When running that I get the expected output.
|
- With an ESM import the top-level object is a Module Namespace Object (https://tc39.es/ecma262/#sec-module-namespace-objects) that has no prototype. For compat, Node.js assigns the usual CommonJS module.exports to `<module>.default`, so use that when this is an ESM module. - Also, TypeScript translates class properties to assignments in the constructor after the super() call. Because super() can call init() and enable() synchronously, it calls back into 'IORedisInstrumentation' before 'traceSendCommand' was defined. Defining it as a method fixes that issue. Fixes: open-telemetry#1692
What version of OpenTelemetry are you using?
What version of Node are you using?
v18.13.0 (18.13.0+dfsg1-1)
What did you do?
create a file
test.mjs
then run:
node --experimental-loader=@opentelemetry/instrumentation/hook.mjs test.mjs
What did you expect to see?
a successfully instrumented ioredis
What did you see instead?
Uncaught TypeError: Cannot read properties of undefined (reading 'sendCommand') instrumentation.js:131
Additional context
when
InstrumentationNodeModuleDefinition
patch callback runs, it expectsrequire-in-the-middle
to provide moduleExports, which is a normal class.However, in ESM-mode,
import-in-the-middle
actually provides a Proxy asmoduleExports
variable, which meansmoduleExports.prototype
is undefined, resulting in an error thrown.The text was updated successfully, but these errors were encountered: