Skip to content

Commit 280e273

Browse files
nicohrubecclaude
andauthored
ref(node): Streamline generic-pool (#21363)
Streamlines the vendored `@opentelemetry/instrumentation-generic-pool`: - Extend test coverage: added a `generic-pool` v2 integration suite (`genericPool-v2`) alongside the existing v3 one, so both major versions are exercised. Ported unit tests from upstream. - Removed the eslint-disable and updated the formatting and types to make the linter happy. This one has three supported version ranges with different instrumentation paths (`2.0 - <2.4`, `2.4 - <3.0`, `>3.0`). The only path that is still heavily used is `>3.0` based on [npm downloads](https://www.npmjs.com/package/generic-pool?activeTab=versions). In the future we can think about dropping v2 support, which would allow us to slim this instrumentation down quite a bit. Closes #20727 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9ead55d commit 280e273

7 files changed

Lines changed: 203 additions & 36 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as Sentry from '@sentry/node';
2+
import genericPool from 'generic-pool';
3+
4+
// generic-pool v2 uses the callback-based API: `new Pool(factory)` with `factory.create(callback)`
5+
// and `pool.acquire((err, client) => ...)`.
6+
const Pool = genericPool.Pool;
7+
8+
const pool = new Pool({
9+
name: 'test',
10+
create: callback => callback(null, { id: Math.random() }),
11+
destroy: () => {},
12+
max: 10,
13+
min: 2,
14+
});
15+
16+
function acquire() {
17+
return new Promise((resolve, reject) => {
18+
pool.acquire((err, client) => {
19+
if (err) {
20+
reject(err);
21+
return;
22+
}
23+
pool.release(client);
24+
resolve();
25+
});
26+
});
27+
}
28+
29+
async function run() {
30+
await Sentry.startSpan(
31+
{
32+
op: 'transaction',
33+
name: 'Test Transaction',
34+
},
35+
async () => {
36+
await acquire();
37+
await acquire();
38+
},
39+
);
40+
}
41+
42+
run();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
3+
4+
describe('genericPool v2 auto instrumentation', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
createEsmAndCjsTests(
10+
__dirname,
11+
'scenario.mjs',
12+
'instrument.mjs',
13+
(createRunner, test) => {
14+
test('should auto-instrument `generic-pool` v2 when calling pool.acquire()', async () => {
15+
const EXPECTED_TRANSACTION = {
16+
transaction: 'Test Transaction',
17+
spans: expect.arrayContaining([
18+
expect.objectContaining({
19+
description: 'generic-pool.acquire',
20+
origin: 'auto.db.otel.generic_pool',
21+
data: {
22+
'sentry.origin': 'auto.db.otel.generic_pool',
23+
},
24+
status: 'ok',
25+
}),
26+
27+
expect.objectContaining({
28+
description: 'generic-pool.acquire',
29+
origin: 'auto.db.otel.generic_pool',
30+
data: {
31+
'sentry.origin': 'auto.db.otel.generic_pool',
32+
},
33+
status: 'ok',
34+
}),
35+
]),
36+
};
37+
38+
await createRunner().expect({ transaction: EXPECTED_TRANSACTION }).start().completed();
39+
});
40+
},
41+
{ additionalDependencies: { 'generic-pool': '^2.5.0' } },
42+
);
43+
});

‎dev-packages/node-integration-tests/suites/tracing/genericPool/test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('genericPool auto instrumentation', () => {
1212
transaction: 'Test Transaction',
1313
spans: expect.arrayContaining([
1414
expect.objectContaining({
15-
description: expect.stringMatching(/^generic-pool\.ac?quire/),
15+
description: 'generic-pool.acquire',
1616
origin: 'auto.db.otel.generic_pool',
1717
data: {
1818
'sentry.origin': 'auto.db.otel.generic_pool',
@@ -21,7 +21,7 @@ describe('genericPool auto instrumentation', () => {
2121
}),
2222

2323
expect.objectContaining({
24-
description: expect.stringMatching(/^generic-pool\.ac?quire/),
24+
description: 'generic-pool.acquire',
2525
origin: 'auto.db.otel.generic_pool',
2626
data: {
2727
'sentry.origin': 'auto.db.otel.generic_pool',

‎packages/node/src/integrations/tracing/genericPool/index.ts‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,8 @@ const _genericPoolIntegration = (() => {
2121
instrumentationWrappedCallback?.(() =>
2222
client.on('spanStart', span => {
2323
const spanJSON = spanToJSON(span);
24-
2524
const spanDescription = spanJSON.description;
26-
27-
// typo in emitted span for version <= 0.38.0 of @opentelemetry/instrumentation-generic-pool
28-
const isGenericPoolSpan =
29-
spanDescription === 'generic-pool.aquire' || spanDescription === 'generic-pool.acquire';
25+
const isGenericPoolSpan = spanDescription === 'generic-pool.acquire';
3026

3127
if (isGenericPoolSpan) {
3228
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.generic_pool');

‎packages/node/src/integrations/tracing/genericPool/vendored/instrumentation.ts‎

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,24 @@
1818
* - Upstream version: @opentelemetry/instrumentation-generic-pool@0.61.0
1919
* - Minor TypeScript strictness adjustments for this repository's compiler settings
2020
*/
21-
/* eslint-disable */
2221

2322
import * as api from '@opentelemetry/api';
24-
import {
25-
InstrumentationBase,
26-
InstrumentationConfig,
27-
InstrumentationNodeModuleDefinition,
28-
isWrapped,
29-
} from '@opentelemetry/instrumentation';
30-
31-
import type * as genericPool from './generic-pool-types';
32-
23+
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
24+
import { InstrumentationBase, InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation';
3325
import { SDK_VERSION } from '@sentry/core';
26+
import type * as genericPool from './generic-pool-types';
3427

3528
const MODULE_NAME = 'generic-pool';
3629
const PACKAGE_NAME = '@sentry/instrumentation-generic-pool';
3730

31+
type AcquireFn = (this: unknown, ...args: unknown[]) => unknown;
32+
interface PoolConstructor {
33+
prototype: { acquire: AcquireFn };
34+
}
35+
interface GenericPoolModule {
36+
Pool: PoolConstructor;
37+
}
38+
3839
export class GenericPoolInstrumentation extends InstrumentationBase {
3940
// only used for v2 - v2.3)
4041
private _isDisabled = false;
@@ -48,49 +49,49 @@ export class GenericPoolInstrumentation extends InstrumentationBase {
4849
new InstrumentationNodeModuleDefinition(
4950
MODULE_NAME,
5051
['>=3.0.0 <4'],
51-
moduleExports => {
52-
const Pool: any = moduleExports.Pool;
52+
(moduleExports: GenericPoolModule) => {
53+
const Pool = moduleExports.Pool;
5354
if (isWrapped(Pool.prototype.acquire)) {
5455
this._unwrap(Pool.prototype, 'acquire');
5556
}
5657
this._wrap(Pool.prototype, 'acquire', this._acquirePatcher.bind(this));
5758
return moduleExports;
5859
},
59-
moduleExports => {
60-
const Pool: any = moduleExports.Pool;
60+
(moduleExports: GenericPoolModule) => {
61+
const Pool = moduleExports.Pool;
6162
this._unwrap(Pool.prototype, 'acquire');
6263
return moduleExports;
6364
},
6465
),
6566
new InstrumentationNodeModuleDefinition(
6667
MODULE_NAME,
6768
['>=2.4.0 <3'],
68-
moduleExports => {
69-
const Pool: any = moduleExports.Pool;
69+
(moduleExports: GenericPoolModule) => {
70+
const Pool = moduleExports.Pool;
7071
if (isWrapped(Pool.prototype.acquire)) {
7172
this._unwrap(Pool.prototype, 'acquire');
7273
}
7374
this._wrap(Pool.prototype, 'acquire', this._acquireWithCallbacksPatcher.bind(this));
7475
return moduleExports;
7576
},
76-
moduleExports => {
77-
const Pool: any = moduleExports.Pool;
77+
(moduleExports: GenericPoolModule) => {
78+
const Pool = moduleExports.Pool;
7879
this._unwrap(Pool.prototype, 'acquire');
7980
return moduleExports;
8081
},
8182
),
8283
new InstrumentationNodeModuleDefinition(
8384
MODULE_NAME,
8485
['>=2.0.0 <2.4'],
85-
moduleExports => {
86+
(moduleExports: GenericPoolModule) => {
8687
this._isDisabled = false;
8788
if (isWrapped(moduleExports.Pool)) {
8889
this._unwrap(moduleExports, 'Pool');
8990
}
9091
this._wrap(moduleExports, 'Pool', this._poolWrapper.bind(this));
9192
return moduleExports;
9293
},
93-
moduleExports => {
94+
(moduleExports: GenericPoolModule) => {
9495
// since the object is created on the fly every time, we need to use
9596
// a boolean switch here to disable the instrumentation
9697
this._isDisabled = true;
@@ -100,14 +101,15 @@ export class GenericPoolInstrumentation extends InstrumentationBase {
100101
];
101102
}
102103

103-
private _acquirePatcher(original: genericPool.Pool<unknown>['acquire']) {
104+
private _acquirePatcher(original: AcquireFn) {
105+
// eslint-disable-next-line @typescript-eslint/no-this-alias
104106
const instrumentation = this;
105-
return function wrapped_acquire(this: genericPool.Pool<unknown>, ...args: any[]) {
107+
return function wrapped_acquire(this: genericPool.Pool<unknown>, ...args: unknown[]) {
106108
const parent = api.context.active();
107109
const span = instrumentation.tracer.startSpan('generic-pool.acquire', {}, parent);
108110

109111
return api.context.with(api.trace.setSpan(parent, span), () => {
110-
return original.call(this, ...args).then(
112+
return (original.call(this, ...args) as PromiseLike<unknown>).then(
111113
(value: unknown) => {
112114
span.end();
113115
return value;
@@ -122,18 +124,24 @@ export class GenericPoolInstrumentation extends InstrumentationBase {
122124
};
123125
}
124126

125-
private _poolWrapper(original: any) {
127+
private _poolWrapper(original: (this: unknown, ...args: unknown[]) => { acquire: AcquireFn }) {
128+
// eslint-disable-next-line @typescript-eslint/no-this-alias
126129
const instrumentation = this;
127-
return function wrapped_pool(this: any) {
128-
const pool = original.apply(this, arguments);
130+
return function wrapped_pool(this: unknown, ...args: unknown[]) {
131+
const pool = original.apply(this, args);
129132
instrumentation._wrap(pool, 'acquire', instrumentation._acquireWithCallbacksPatcher.bind(instrumentation));
130133
return pool;
131134
};
132135
}
133136

134-
private _acquireWithCallbacksPatcher(original: any) {
137+
private _acquireWithCallbacksPatcher(original: AcquireFn) {
138+
// eslint-disable-next-line @typescript-eslint/no-this-alias
135139
const instrumentation = this;
136-
return function wrapped_acquire(this: genericPool.Pool<unknown>, cb: Function, priority: number) {
140+
return function wrapped_acquire(
141+
this: genericPool.Pool<unknown>,
142+
cb: (err: unknown, client: unknown) => unknown,
143+
priority: number,
144+
) {
137145
// only used for v2 - v2.3
138146
if (instrumentation._isDisabled) {
139147
return original.call(this, cb, priority);
@@ -148,8 +156,9 @@ export class GenericPoolInstrumentation extends InstrumentationBase {
148156
span.end();
149157
// Not checking whether cb is a function because
150158
// the original code doesn't do that either.
159+
// The callback's return value is unused by generic-pool, so we don't return it.
151160
if (cb) {
152-
return cb(err, client);
161+
cb(err, client);
153162
}
154163
},
155164
priority,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Tests ported from @opentelemetry/instrumentation-generic-pool@0.61.0
3+
* Original source: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-generic-pool
4+
* Licensed under the Apache License, Version 2.0
5+
*/
6+
7+
import { context, trace } from '@opentelemetry/api';
8+
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
9+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10+
import { GenericPoolInstrumentation } from '../../../src/integrations/tracing/genericPool/vendored/instrumentation';
11+
import { cleanupOtel, mockSdkInit } from '../../helpers/mockSdkInit';
12+
13+
// Create a fake `generic-pool` module.
14+
function createPoolModule(): { Pool: new () => { acquire: () => Promise<string> } } {
15+
class FakePool {
16+
public acquire(): Promise<string> {
17+
return Promise.resolve('client');
18+
}
19+
}
20+
return { Pool: FakePool };
21+
}
22+
23+
describe('GenericPoolInstrumentation', () => {
24+
const memoryExporter = new InMemorySpanExporter();
25+
const provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(memoryExporter)] });
26+
27+
let instrumentation: GenericPoolInstrumentation;
28+
29+
beforeEach(() => {
30+
mockSdkInit({ tracesSampleRate: 1 });
31+
instrumentation = new GenericPoolInstrumentation();
32+
instrumentation.setTracerProvider(provider);
33+
memoryExporter.reset();
34+
});
35+
36+
afterEach(() => {
37+
instrumentation.disable();
38+
cleanupOtel();
39+
});
40+
41+
it('should attach it to the parent span', async () => {
42+
const moduleExports = createPoolModule();
43+
const def = instrumentation.getModuleDefinitions()[0]!;
44+
def.patch!(moduleExports);
45+
46+
const parent = provider.getTracer('test').startSpan('parent');
47+
await context.with(trace.setSpan(context.active(), parent), async () => {
48+
await new moduleExports.Pool().acquire();
49+
});
50+
parent.end();
51+
52+
const acquireSpan = memoryExporter.getFinishedSpans().find(span => span.name === 'generic-pool.acquire');
53+
expect(acquireSpan).toBeDefined();
54+
expect(acquireSpan!.parentSpanContext?.spanId).toBe(parent.spanContext().spanId);
55+
});
56+
57+
it('should not create anything if disabled', async () => {
58+
const moduleExports = createPoolModule();
59+
const def = instrumentation.getModuleDefinitions()[0]!;
60+
// Patch then unpatch to mimic the instrumentation being disabled.
61+
def.patch!(moduleExports);
62+
def.unpatch!(moduleExports);
63+
64+
await new moduleExports.Pool().acquire();
65+
66+
expect(memoryExporter.getFinishedSpans()).toHaveLength(0);
67+
});
68+
});

0 commit comments

Comments
 (0)