Skip to content

feat(node): Only add span listeners for instrumentation when used #15802

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

Merged
merged 4 commits into from
Mar 26, 2025
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
2 changes: 1 addition & 1 deletion packages/node/src/integrations/tracing/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function addConnectSpanAttributes(span: Span): void {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: `${type}.connect`,
});

// Also update the name, we don't need to "middleware - " prefix
// Also update the name, we don't need the "middleware - " prefix
const name = attributes['connect.name'];
if (typeof name === 'string') {
span.updateName(name);
Expand Down
44 changes: 25 additions & 19 deletions packages/node/src/integrations/tracing/dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
spanToJSON,
} from '@sentry/core';
import type { IntegrationFn } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { instrumentWhenWrapped, generateInstrumentOnce } from '../../otel/instrument';

const INTEGRATION_NAME = 'Dataloader';

Expand All @@ -19,31 +19,37 @@ export const instrumentDataloader = generateInstrumentOnce(
);

const _dataloaderIntegration = (() => {
let instrumentationWrappedCallback: undefined | ((callback: () => void) => void);

return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentDataloader();
const instrumentation = instrumentDataloader();
instrumentationWrappedCallback = instrumentWhenWrapped(instrumentation);
},

setup(client) {
client.on('spanStart', span => {
const spanJSON = spanToJSON(span);
if (spanJSON.description?.startsWith('dataloader')) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.dataloader');
}
// This is called either immediately or when the instrumentation is wrapped
instrumentationWrappedCallback?.(() => {
client.on('spanStart', span => {
const spanJSON = spanToJSON(span);
if (spanJSON.description?.startsWith('dataloader')) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.dataloader');
}

// These are all possible dataloader span descriptions
// Still checking for the future versions
// in case they add support for `clear` and `prime`
if (
spanJSON.description === 'dataloader.load' ||
spanJSON.description === 'dataloader.loadMany' ||
spanJSON.description === 'dataloader.batch'
) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'cache.get');
// TODO: We can try adding `key` to the `data` attribute upstream.
// Or alternatively, we can add `requestHook` to the dataloader instrumentation.
}
// These are all possible dataloader span descriptions
// Still checking for the future versions
// in case they add support for `clear` and `prime`
if (
spanJSON.description === 'dataloader.load' ||
spanJSON.description === 'dataloader.loadMany' ||
spanJSON.description === 'dataloader.batch'
) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'cache.get');
// TODO: We can try adding `key` to the `data` attribute upstream.
// Or alternatively, we can add `requestHook` to the dataloader instrumentation.
}
});
});
},
};
Expand Down
29 changes: 17 additions & 12 deletions packages/node/src/integrations/tracing/genericPool.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core';
import type { IntegrationFn } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';

const INTEGRATION_NAME = 'GenericPool';

export const instrumentGenericPool = generateInstrumentOnce(INTEGRATION_NAME, () => new GenericPoolInstrumentation({}));

const _genericPoolIntegration = (() => {
let instrumentationWrappedCallback: undefined | ((callback: () => void) => void);

return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentGenericPool();
const instrumentation = instrumentGenericPool();
instrumentationWrappedCallback = instrumentWhenWrapped(instrumentation);
},

setup(client) {
client.on('spanStart', span => {
const spanJSON = spanToJSON(span);
instrumentationWrappedCallback?.(() =>
client.on('spanStart', span => {
const spanJSON = spanToJSON(span);

const spanDescription = spanJSON.description;
const spanDescription = spanJSON.description;

// typo in emitted span for version <= 0.38.0 of @opentelemetry/instrumentation-generic-pool
const isGenericPoolSpan =
spanDescription === 'generic-pool.aquire' || spanDescription === 'generic-pool.acquire';
// typo in emitted span for version <= 0.38.0 of @opentelemetry/instrumentation-generic-pool
const isGenericPoolSpan =
spanDescription === 'generic-pool.aquire' || spanDescription === 'generic-pool.acquire';

if (isGenericPoolSpan) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.generic_pool');
}
});
if (isGenericPoolSpan) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.generic_pool');
}
}),
);
},
};
}) satisfies IntegrationFn;
Expand Down
25 changes: 15 additions & 10 deletions packages/node/src/integrations/tracing/knex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core';
import type { IntegrationFn } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';

const INTEGRATION_NAME = 'Knex';

Expand All @@ -11,21 +11,26 @@ export const instrumentKnex = generateInstrumentOnce(
);

const _knexIntegration = (() => {
let instrumentationWrappedCallback: undefined | ((callback: () => void) => void);

return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentKnex();
const instrumentation = instrumentKnex();
instrumentationWrappedCallback = instrumentWhenWrapped(instrumentation);
},

setup(client) {
client.on('spanStart', span => {
const { data } = spanToJSON(span);
// knex.version is always set in the span data
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/0309caeafc44ac9cb13a3345b790b01b76d0497d/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts#L138
if ('knex.version' in data) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.knex');
}
});
instrumentationWrappedCallback?.(() =>
client.on('spanStart', span => {
const { data } = spanToJSON(span);
// knex.version is always set in the span data
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/0309caeafc44ac9cb13a3345b790b01b76d0497d/plugins/node/opentelemetry-instrumentation-knex/src/instrumentation.ts#L138
if ('knex.version' in data) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.knex');
}
}),
);
},
};
}) satisfies IntegrationFn;
Expand Down
26 changes: 19 additions & 7 deletions packages/node/src/integrations/tracing/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ function isPrismaV6TracingHelper(helper: unknown): helper is PrismaV6TracingHelp
return !!helper && typeof helper === 'object' && 'dispatchEngineSpans' in helper;
}

function getPrismaTracingHelper(): unknown | undefined {
const prismaInstrumentationObject = (globalThis as Record<string, unknown>).PRISMA_INSTRUMENTATION;
const prismaTracingHelper =
prismaInstrumentationObject &&
typeof prismaInstrumentationObject === 'object' &&
'helper' in prismaInstrumentationObject
? prismaInstrumentationObject.helper
: undefined;

return prismaTracingHelper;
}

class SentryPrismaInteropInstrumentation extends EsmInteropPrismaInstrumentation {
public constructor() {
super();
Expand All @@ -30,13 +42,7 @@ class SentryPrismaInteropInstrumentation extends EsmInteropPrismaInstrumentation
// The PrismaIntegration (super class) defines a global variable `global["PRISMA_INSTRUMENTATION"]` when `enable()` is called. This global variable holds a "TracingHelper" which Prisma uses internally to create tracing data. It's their way of not depending on OTEL with their main package. The sucky thing is, prisma broke the interface of the tracing helper with the v6 major update. This means that if you use Prisma 5 with the v6 instrumentation (or vice versa) Prisma just blows up, because tries to call methods on the helper that no longer exist.
// Because we actually want to use the v6 instrumentation and not blow up in Prisma 5 user's faces, what we're doing here is backfilling the v5 method (`createEngineSpan`) with a noop so that no longer crashes when it attempts to call that function.
// We still won't fully emit all the spans, but this could potentially be implemented in the future.
const prismaInstrumentationObject = (globalThis as Record<string, unknown>).PRISMA_INSTRUMENTATION;
const prismaTracingHelper =
prismaInstrumentationObject &&
typeof prismaInstrumentationObject === 'object' &&
'helper' in prismaInstrumentationObject
? prismaInstrumentationObject.helper
: undefined;
const prismaTracingHelper = getPrismaTracingHelper();

let emittedWarning = false;

Expand Down Expand Up @@ -119,6 +125,12 @@ export const prismaIntegration = defineIntegration(
instrumentPrisma({ prismaInstrumentation });
},
setup(client) {
// If no tracing helper exists, we skip any work here
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not perfect as possibly prisma could be set up later than setup(client) is being called, but it was the best I could come up with here 🤔 it seems to work in tests at least!

// this means that prisma is not being used
if (!getPrismaTracingHelper()) {
return;
}

client.on('spanStart', span => {
const spanJSON = spanToJSON(span);
if (spanJSON.description?.startsWith('prisma:')) {
Expand Down
33 changes: 19 additions & 14 deletions packages/node/src/integrations/tracing/tedious.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TediousInstrumentation } from '@opentelemetry/instrumentation-tedious';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, spanToJSON } from '@sentry/core';
import type { IntegrationFn } from '@sentry/core';
import { generateInstrumentOnce } from '../../otel/instrument';
import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument';

const TEDIUS_INSTRUMENTED_METHODS = new Set([
'callProcedure',
Expand All @@ -17,25 +17,30 @@ const INTEGRATION_NAME = 'Tedious';
export const instrumentTedious = generateInstrumentOnce(INTEGRATION_NAME, () => new TediousInstrumentation({}));

const _tediousIntegration = (() => {
let instrumentationWrappedCallback: undefined | ((callback: () => void) => void);

return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentTedious();
const instrumentation = instrumentTedious();
instrumentationWrappedCallback = instrumentWhenWrapped(instrumentation);
},

setup(client) {
client.on('spanStart', span => {
const { description, data } = spanToJSON(span);
// Tedius integration always set a span name and `db.system` attribute to `mssql`.
if (!description || data['db.system'] !== 'mssql') {
return;
}

const operation = description.split(' ')[0] || '';
if (TEDIUS_INSTRUMENTED_METHODS.has(operation)) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.tedious');
}
});
instrumentationWrappedCallback?.(() =>
client.on('spanStart', span => {
const { description, data } = spanToJSON(span);
// Tedius integration always set a span name and `db.system` attribute to `mssql`.
if (!description || data['db.system'] !== 'mssql') {
return;
}

const operation = description.split(' ')[0] || '';
if (TEDIUS_INSTRUMENTED_METHODS.has(operation)) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.tedious');
}
}),
);
},
};
}) satisfies IntegrationFn;
Expand Down
Loading
Loading