Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { afterAll, describe, expect } from 'vitest';
import { isOrchestrionEnabled } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('genericPool v2 auto instrumentation', () => {
afterAll(() => {
cleanupChildProcesses();
});

// The orchestrion channel integration replaces the OTel one 1:1 but tags spans with its own origin.
const ORIGIN = isOrchestrionEnabled() ? 'auto.db.orchestrion.generic_pool' : 'auto.db.otel.generic_pool';

createEsmAndCjsTests(
__dirname,
'scenario.mjs',
Expand All @@ -17,18 +21,18 @@ describe('genericPool v2 auto instrumentation', () => {
spans: expect.arrayContaining([
expect.objectContaining({
description: 'generic-pool.acquire',
origin: 'auto.db.otel.generic_pool',
origin: ORIGIN,
data: {
'sentry.origin': 'auto.db.otel.generic_pool',
'sentry.origin': ORIGIN,
},
status: 'ok',
}),

expect.objectContaining({
description: 'generic-pool.acquire',
origin: 'auto.db.otel.generic_pool',
origin: ORIGIN,
data: {
'sentry.origin': 'auto.db.otel.generic_pool',
'sentry.origin': ORIGIN,
},
status: 'ok',
}),
Expand All @@ -47,15 +51,18 @@ describe('genericPool v2 auto instrumentation', () => {
'instrument.mjs',
(createRunner, test) => {
test('marks the `generic-pool.acquire` span as errored when acquiring fails', async () => {
// The orchestrion path also records the rejection's `error.type` on the span.
const errorData = isOrchestrionEnabled()
? { 'sentry.origin': ORIGIN, 'error.type': 'Error' }
: { 'sentry.origin': ORIGIN };

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'generic-pool.acquire',
origin: 'auto.db.otel.generic_pool',
data: {
'sentry.origin': 'auto.db.otel.generic_pool',
},
origin: ORIGIN,
data: errorData,
status: 'internal_error',
}),
]),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { afterAll, describe, expect } from 'vitest';
import { isOrchestrionEnabled } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('genericPool auto instrumentation', () => {
afterAll(() => {
cleanupChildProcesses();
});

// The orchestrion channel integration replaces the OTel one 1:1 but tags spans with its own origin.
const ORIGIN = isOrchestrionEnabled() ? 'auto.db.orchestrion.generic_pool' : 'auto.db.otel.generic_pool';

createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => {
test('should auto-instrument `genericPool` package when calling pool.require()', async () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'generic-pool.acquire',
origin: 'auto.db.otel.generic_pool',
origin: ORIGIN,
data: {
'sentry.origin': 'auto.db.otel.generic_pool',
'sentry.origin': ORIGIN,
},
status: 'ok',
}),

expect.objectContaining({
description: 'generic-pool.acquire',
origin: 'auto.db.otel.generic_pool',
origin: ORIGIN,
data: {
'sentry.origin': 'auto.db.otel.generic_pool',
'sentry.origin': ORIGIN,
},
status: 'ok',
}),
Expand All @@ -37,15 +41,18 @@ describe('genericPool auto instrumentation', () => {

createEsmAndCjsTests(__dirname, 'scenario-error.mjs', 'instrument.mjs', (createRunner, test) => {
test('marks the `generic-pool.acquire` span as errored when acquiring fails', async () => {
// The orchestrion path also records the rejection's `error.type` on the span.
const errorData = isOrchestrionEnabled()
? { 'sentry.origin': ORIGIN, 'error.type': 'TimeoutError' }
: { 'sentry.origin': ORIGIN };

const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'generic-pool.acquire',
origin: 'auto.db.otel.generic_pool',
data: {
'sentry.origin': 'auto.db.otel.generic_pool',
},
origin: ORIGIN,
data: errorData,
status: 'internal_error',
}),
]),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as diagnosticsChannel from 'node:diagnostics_channel';
import type { IntegrationFn } from '@sentry/core';
import {
defineIntegration,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
waitForTracingChannelBinding,
} from '@sentry/core';
import { CHANNELS } from '../../orchestrion/channels';
import { bindTracingChannelToSpan } from '../../tracing-channel';

// Same name as the OTel integration by design — when enabled, the OTel
// 'GenericPool' integration is omitted from the default set.
const INTEGRATION_NAME = 'GenericPool' as const;

interface GenericPoolAcquireContext {
arguments: unknown[];
}

const _genericPoolChannelIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
// `tracingChannel` is unavailable before Node 18.19 so do nothing in that case.
if (!diagnosticsChannel.tracingChannel) {
return;
}

waitForTracingChannelBinding(() => instrumentGenericPool());
},
};
}) satisfies IntegrationFn;

/**
* EXPERIMENTAL — orchestrion-driven generic-pool integration. Subscribes to
* `orchestrion:generic-pool:acquire` (injected into `generic-pool/lib/Pool.js`'s
* `Pool.prototype.acquire`). Creates a `generic-pool.acquire` span for each
* acquisition. Requires the orchestrion runtime hook or bundler plugin.
*/
export const genericPoolChannelIntegration = defineIntegration(_genericPoolChannelIntegration);

function instrumentGenericPool(): void {
bindTracingChannelToSpan(
diagnosticsChannel.tracingChannel<GenericPoolAcquireContext>(CHANNELS.GENERIC_POOL_ACQUIRE),
() =>
startInactiveSpan({
name: 'generic-pool.acquire',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.orchestrion.generic_pool',
},
}),
);
}
23 changes: 20 additions & 3 deletions packages/server-utils/src/orchestrion/config/generic-pool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';

// TODO: Stub for the `generic-pool` orchestrion integration (ports `@opentelemetry/instrumentation-generic-pool`).
export const genericPoolConfig: InstrumentationConfig[] = [];
// Two shapes of `acquire`, both publishing to the same `orchestrion:generic-pool:acquire` channel:
// - v3+: `class Pool { acquire(priority) }` returns a promise, so `kind: 'Auto'` resolves to `wrapPromise`.
// - v2.4–v3: `Pool.prototype.acquire = function acquire(callback, priority)` is callback-based.
// Versions before 2.4 assigned an anonymous `acquire` per pool instance (a factory), which a static
// transform can't target, so they're out of scope (matching how the OTel instrumentation split them).
export const genericPoolConfig = [
{
channelName: 'acquire',
module: { name: 'generic-pool', versionRange: '>=3.0.0 <4', filePath: 'lib/Pool.js' },
functionQuery: { className: 'Pool', methodName: 'acquire', kind: 'Auto' },
},
{
channelName: 'acquire',
module: { name: 'generic-pool', versionRange: '>=2.4.0 <3', filePath: 'lib/generic-pool.js' },
functionQuery: { expressionName: 'acquire', kind: 'Callback' },
},
] satisfies InstrumentationConfig[];

export const genericPoolChannels = {} as const;
export const genericPoolChannels = {
GENERIC_POOL_ACQUIRE: 'orchestrion:generic-pool:acquire',
} as const;
3 changes: 3 additions & 0 deletions packages/server-utils/src/orchestrion/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { amqplibChannelIntegration } from '../integrations/tracing-channel/amqplib';
import { anthropicChannelIntegration } from '../integrations/tracing-channel/anthropic';
import { genericPoolChannelIntegration } from '../integrations/tracing-channel/generic-pool';
import { googleGenAIChannelIntegration } from '../integrations/tracing-channel/google-genai';
import {
graphqlChannelIntegration,
Expand All @@ -23,6 +24,7 @@ export { nestjsChannels } from './config/nestjs';
export {
amqplibChannelIntegration,
anthropicChannelIntegration,
genericPoolChannelIntegration,
googleGenAIChannelIntegration,
graphqlChannelIntegration,
hapiChannelIntegration,
Expand Down Expand Up @@ -66,6 +68,7 @@ export const channelIntegrations = {
postgresIntegration: postgresChannelIntegration,
postgresJsIntegration: postgresJsChannelIntegration,
mysqlIntegration: mysqlChannelIntegration,
genericPoolIntegration: genericPoolChannelIntegration,
lruMemoizerIntegration: lruMemoizerChannelIntegration,
openaiIntegration: openaiChannelIntegration,
anthropicIntegration: anthropicChannelIntegration,
Expand Down
Loading