Skip to content

Commit acfeb34

Browse files
committed
fix(instrumentation-dataloder): Patch batchLoadFn without creating an instance
1 parent d056d21 commit acfeb34

File tree

2 files changed

+81
-47
lines changed

2 files changed

+81
-47
lines changed

plugins/node/instrumentation-dataloader/src/instrumentation.ts

Lines changed: 56 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -90,64 +90,73 @@ export class DataloaderInstrumentation extends InstrumentationBase<DataloaderIns
9090
return `${MODULE_NAME}.${operation} ${dataloaderName}`;
9191
}
9292

93+
private _wrapBatchLoadFn(
94+
batchLoadFn: Dataloader.BatchLoadFn<unknown, unknown>
95+
): Dataloader.BatchLoadFn<unknown, unknown> {
96+
const instrumentation = this;
97+
98+
return function patchedBatchLoadFn(
99+
this: DataloaderInternal,
100+
...args: Parameters<Dataloader.BatchLoadFn<unknown, unknown>>
101+
) {
102+
if (
103+
!instrumentation.isEnabled() ||
104+
!instrumentation.shouldCreateSpans()
105+
) {
106+
return batchLoadFn.call(this, ...args);
107+
}
108+
109+
const parent = context.active();
110+
const span = instrumentation.tracer.startSpan(
111+
instrumentation.getSpanName(this, 'batch'),
112+
{ links: this._batch?.spanLinks as Link[] | undefined },
113+
parent
114+
);
115+
116+
return context.with(trace.setSpan(parent, span), () => {
117+
return (batchLoadFn.apply(this, args) as Promise<unknown[]>)
118+
.then(value => {
119+
span.end();
120+
return value;
121+
})
122+
.catch(err => {
123+
span.recordException(err);
124+
span.setStatus({
125+
code: SpanStatusCode.ERROR,
126+
message: err.message,
127+
});
128+
span.end();
129+
throw err;
130+
});
131+
});
132+
};
133+
}
134+
93135
private _getPatchedConstructor(
94136
constructor: typeof Dataloader
95137
): typeof Dataloader {
96-
const prototype = constructor.prototype;
97138
const instrumentation = this;
139+
const prototype = constructor.prototype;
140+
141+
if (!instrumentation.isEnabled() || !instrumentation.shouldCreateSpans()) {
142+
return constructor;
143+
}
98144

99145
function PatchedDataloader(
146+
this: DataloaderInternal,
100147
...args: ConstructorParameters<typeof constructor>
101148
) {
102-
const inst = new constructor(...args) as DataloaderInternal;
103-
104-
if (!instrumentation.isEnabled()) {
105-
return inst;
106-
}
107-
108-
if (isWrapped(inst._batchLoadFn)) {
109-
instrumentation._unwrap(inst, '_batchLoadFn');
149+
// BatchLoadFn is the first constructor argument
150+
// https://github.com/graphql/dataloader/blob/77c2cd7ca97e8795242018ebc212ce2487e729d2/src/index.js#L47
151+
if (isWrapped(args[0])) {
152+
instrumentation._unwrap(args, 0);
110153
}
111154

112-
instrumentation._wrap(inst, '_batchLoadFn', original => {
113-
return function patchedBatchLoadFn(
114-
this: DataloaderInternal,
115-
...args: Parameters<Dataloader.BatchLoadFn<unknown, unknown>>
116-
) {
117-
if (
118-
!instrumentation.isEnabled() ||
119-
!instrumentation.shouldCreateSpans()
120-
) {
121-
return original.call(this, ...args);
122-
}
123-
124-
const parent = context.active();
125-
const span = instrumentation.tracer.startSpan(
126-
instrumentation.getSpanName(inst, 'batch'),
127-
{ links: this._batch?.spanLinks as Link[] | undefined },
128-
parent
129-
);
130-
131-
return context.with(trace.setSpan(parent, span), () => {
132-
return (original.apply(this, args) as Promise<unknown[]>)
133-
.then(value => {
134-
span.end();
135-
return value;
136-
})
137-
.catch(err => {
138-
span.recordException(err);
139-
span.setStatus({
140-
code: SpanStatusCode.ERROR,
141-
message: err.message,
142-
});
143-
span.end();
144-
throw err;
145-
});
146-
});
147-
};
148-
});
155+
args[0] = instrumentation._wrapBatchLoadFn(
156+
args[0]
157+
) as Dataloader.BatchLoadFn<unknown, unknown>;
149158

150-
return inst;
159+
return constructor.apply(this, args);
151160
}
152161

153162
PatchedDataloader.prototype = prototype;

plugins/node/instrumentation-dataloader/test/dataloader.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,25 @@ extraInstrumentation.disable();
3131

3232
import * as assert from 'assert';
3333
import * as Dataloader from 'dataloader';
34+
import * as crypto from 'crypto';
35+
36+
const getMd5HashFromIdx = (idx: number) =>
37+
crypto.createHash('md5').update(String(idx)).digest('hex');
3438

3539
describe('DataloaderInstrumentation', () => {
3640
let dataloader: Dataloader<string, number>;
3741
let contextManager: AsyncHooksContextManager;
3842

43+
class CustomDataLoader extends Dataloader<string, string> {
44+
constructor() {
45+
super(async keys => keys.map((_, idx) => getMd5HashFromIdx(idx)));
46+
}
47+
48+
public async customLoad() {
49+
return this.load('test');
50+
}
51+
}
52+
3953
const memoryExporter = new InMemorySpanExporter();
4054
const provider = new NodeTracerProvider();
4155
const tracer = provider.getTracer('default');
@@ -334,4 +348,15 @@ describe('DataloaderInstrumentation', () => {
334348
assert.deepStrictEqual(await alternativeDataloader.loadMany(['test']), [1]);
335349
assert.strictEqual(memoryExporter.getFinishedSpans().length, 5);
336350
});
351+
352+
it('should not prune custom methods', async () => {
353+
const customDataloader = new CustomDataLoader();
354+
await customDataloader.customLoad();
355+
356+
assert.strictEqual(memoryExporter.getFinishedSpans().length, 2);
357+
const [batchSpan, loadSpan] = memoryExporter.getFinishedSpans();
358+
359+
assert.strictEqual(loadSpan.name, 'dataloader.load');
360+
assert.strictEqual(batchSpan.name, 'dataloader.batch');
361+
});
337362
});

0 commit comments

Comments
 (0)