diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index 82f9f4ac5d50..5ba8e4bd2fe5 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -147,7 +147,8 @@ "**/integrations/fs/vendored/**/*.ts", "**/integrations/tracing/knex/vendored/**/*.ts", "**/integrations/tracing/mongo/vendored/**/*.ts", - "**/integrations/tracing/connect/vendored/**/*.ts" + "**/integrations/tracing/connect/vendored/**/*.ts", + "**/nestjs/src/integrations/vendored/**/*.ts" ], "rules": { "typescript/no-explicit-any": "off" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 432acc2b89ed..b996a8c56b28 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -47,7 +47,6 @@ "@opentelemetry/api": "^1.9.1", "@opentelemetry/core": "^2.6.1", "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/instrumentation-nestjs-core": "0.60.0", "@opentelemetry/semantic-conventions": "^1.40.0", "@sentry/core": "10.53.1", "@sentry/node": "10.53.1" diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts index 330c76f319cb..ec351f66348b 100644 --- a/packages/nestjs/src/integrations/nest.ts +++ b/packages/nestjs/src/integrations/nest.ts @@ -1,4 +1,4 @@ -import { NestInstrumentation as NestInstrumentationCore } from '@opentelemetry/instrumentation-nestjs-core'; +import { NestInstrumentation as NestInstrumentationCore } from './vendored/instrumentation'; import { defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node'; import { SentryNestBullMQInstrumentation } from './sentry-nest-bullmq-instrumentation'; diff --git a/packages/nestjs/src/integrations/vendored/enums/AttributeNames.ts b/packages/nestjs/src/integrations/vendored/enums/AttributeNames.ts new file mode 100644 index 000000000000..c9b0b04a839e --- /dev/null +++ b/packages/nestjs/src/integrations/vendored/enums/AttributeNames.ts @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core + * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 + */ +/* eslint-disable */ + +export enum AttributeNames { + VERSION = 'nestjs.version', + TYPE = 'nestjs.type', + MODULE = 'nestjs.module', + CONTROLLER = 'nestjs.controller', + CALLBACK = 'nestjs.callback', + PIPES = 'nestjs.pipes', + INTERCEPTORS = 'nestjs.interceptors', + GUARDS = 'nestjs.guards', +} diff --git a/packages/nestjs/src/integrations/vendored/enums/NestType.ts b/packages/nestjs/src/integrations/vendored/enums/NestType.ts new file mode 100644 index 000000000000..1a1dc69c8e4c --- /dev/null +++ b/packages/nestjs/src/integrations/vendored/enums/NestType.ts @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core + * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 + */ +/* eslint-disable */ + +export enum NestType { + APP_CREATION = 'app_creation', + REQUEST_CONTEXT = 'request_context', + REQUEST_HANDLER = 'handler', +} diff --git a/packages/nestjs/src/integrations/vendored/enums/index.ts b/packages/nestjs/src/integrations/vendored/enums/index.ts new file mode 100644 index 000000000000..18970ae86ba2 --- /dev/null +++ b/packages/nestjs/src/integrations/vendored/enums/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core + * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 + */ +/* eslint-disable */ + +export { AttributeNames } from './AttributeNames'; +export { NestType } from './NestType'; diff --git a/packages/nestjs/src/integrations/vendored/instrumentation.ts b/packages/nestjs/src/integrations/vendored/instrumentation.ts new file mode 100644 index 000000000000..1dc6f08809fd --- /dev/null +++ b/packages/nestjs/src/integrations/vendored/instrumentation.ts @@ -0,0 +1,249 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core + * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 + * - Some types vendored from @nestjs/core and @nestjs/common with simplifications + */ +/* eslint-disable */ + +import * as api from '@opentelemetry/api'; +import { + InstrumentationBase, + InstrumentationConfig, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, + SemconvStability, + semconvStabilityFromStr, +} from '@opentelemetry/instrumentation'; +import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, ATTR_URL_FULL } from '@opentelemetry/semantic-conventions'; +import { SDK_VERSION } from '@sentry/core'; +import { ATTR_HTTP_METHOD, ATTR_HTTP_URL } from './semconv'; +import { AttributeNames, NestType } from './enums'; + +const PACKAGE_NAME = '@sentry/instrumentation-nestjs-core'; + +type Controller = object; + +declare const NestFactory: { + create(...args: any[]): Promise; +}; + +interface RouterExecutionContext { + create(instance: Controller, callback: (...args: any[]) => unknown, ...args: any[]): any; +} + +declare namespace Reflect { + function getMetadataKeys(target: any): any[]; + function getMetadata(metadataKey: any, target: any): any; + function defineMetadata(metadataKey: any, metadataValue: any, target: any): void; +} + +const supportedVersions = ['>=4.0.0 <12']; + +export class NestInstrumentation extends InstrumentationBase { + static readonly COMPONENT = '@nestjs/core'; + static readonly COMMON_ATTRIBUTES = { + component: NestInstrumentation.COMPONENT, + }; + + private _semconvStability: SemconvStability; + + constructor(config: InstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, config); + this._semconvStability = semconvStabilityFromStr('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN); + } + + init() { + const module = new InstrumentationNodeModuleDefinition(NestInstrumentation.COMPONENT, supportedVersions); + + module.files.push( + this.getNestFactoryFileInstrumentation(supportedVersions), + this.getRouterExecutionContextFileInstrumentation(supportedVersions), + ); + + return module; + } + + getNestFactoryFileInstrumentation(versions: string[]) { + return new InstrumentationNodeModuleFile( + '@nestjs/core/nest-factory.js', + versions, + (NestFactoryStatic: any, moduleVersion?: string) => { + this.ensureWrapped( + NestFactoryStatic.NestFactoryStatic.prototype, + 'create', + createWrapNestFactoryCreate(this.tracer, moduleVersion), + ); + return NestFactoryStatic; + }, + (NestFactoryStatic: any) => { + this._unwrap(NestFactoryStatic.NestFactoryStatic.prototype, 'create'); + }, + ); + } + + getRouterExecutionContextFileInstrumentation(versions: string[]) { + return new InstrumentationNodeModuleFile( + '@nestjs/core/router/router-execution-context.js', + versions, + (RouterExecutionContext: any, moduleVersion?: string) => { + this.ensureWrapped( + RouterExecutionContext.RouterExecutionContext.prototype, + 'create', + createWrapCreateHandler(this.tracer, moduleVersion, this._semconvStability), + ); + return RouterExecutionContext; + }, + (RouterExecutionContext: any) => { + this._unwrap(RouterExecutionContext.RouterExecutionContext.prototype, 'create'); + }, + ); + } + + private ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any) { + if (isWrapped(obj[methodName])) { + this._unwrap(obj, methodName); + } + this._wrap(obj, methodName, wrapper); + } +} + +function createWrapNestFactoryCreate(tracer: api.Tracer, moduleVersion?: string) { + return function wrapCreate(original: typeof NestFactory.create) { + return function createWithTrace( + this: typeof NestFactory, + nestModule: any, + /* serverOrOptions */ + ) { + const span = tracer.startSpan('Create Nest App', { + attributes: { + ...NestInstrumentation.COMMON_ATTRIBUTES, + [AttributeNames.TYPE]: NestType.APP_CREATION, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.MODULE]: nestModule.name, + }, + }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + return await original.apply(this, arguments as any); + } catch (e: any) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + }; +} + +function createWrapCreateHandler( + tracer: api.Tracer, + moduleVersion: string | undefined, + semconvStability: SemconvStability, +) { + return function wrapCreateHandler(original: RouterExecutionContext['create']) { + return function createHandlerWithTrace( + this: RouterExecutionContext, + instance: Controller, + callback: (...args: any[]) => unknown, + ) { + arguments[1] = createWrapHandler(tracer, moduleVersion, callback); + const handler = original.apply(this, arguments as any); + const callbackName = callback.name; + const instanceName = + instance.constructor && instance.constructor.name ? instance.constructor.name : 'UnnamedInstance'; + const spanName = callbackName ? `${instanceName}.${callbackName}` : instanceName; + + return function (this: any, req: any, res: any, next: (...args: any[]) => unknown) { + const attributes: api.Attributes = { + ...NestInstrumentation.COMMON_ATTRIBUTES, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, + [ATTR_HTTP_ROUTE]: req.route?.path || req.routeOptions?.url || req.routerPath, + [AttributeNames.CONTROLLER]: instanceName, + [AttributeNames.CALLBACK]: callbackName, + }; + if (semconvStability & SemconvStability.OLD) { + attributes[ATTR_HTTP_METHOD] = req.method; + attributes[ATTR_HTTP_URL] = req.originalUrl || req.url; + } + if (semconvStability & SemconvStability.STABLE) { + attributes[ATTR_HTTP_REQUEST_METHOD] = req.method; + attributes[ATTR_URL_FULL] = req.originalUrl || req.url; + } + const span = tracer.startSpan(spanName, { attributes }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + return await handler.apply(this, arguments as any); + } catch (e: any) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + }; + }; +} + +function createWrapHandler(tracer: api.Tracer, moduleVersion: string | undefined, handler: Function) { + const spanName = handler.name || 'anonymous nest handler'; + const options = { + attributes: { + ...NestInstrumentation.COMMON_ATTRIBUTES, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.TYPE]: NestType.REQUEST_HANDLER, + [AttributeNames.CALLBACK]: handler.name, + }, + }; + const wrappedHandler = function (this: RouterExecutionContext) { + const span = tracer.startSpan(spanName, options); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + return await handler.apply(this, arguments); + } catch (e: any) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + + if (handler.name) { + Object.defineProperty(wrappedHandler, 'name', { value: handler.name }); + } + + // Get the current metadata and set onto the wrapper to ensure other decorators ( ie: NestJS EventPattern / RolesGuard ) + // won't be affected by the use of this instrumentation + Reflect.getMetadataKeys(handler).forEach(metadataKey => { + Reflect.defineMetadata(metadataKey, Reflect.getMetadata(metadataKey, handler), wrappedHandler); + }); + return wrappedHandler; +} + +const addError = (span: api.Span, error: Error) => { + span.recordException(error); + span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); + return error; +}; diff --git a/packages/nestjs/src/integrations/vendored/semconv.ts b/packages/nestjs/src/integrations/vendored/semconv.ts new file mode 100644 index 000000000000..52d15d084d0b --- /dev/null +++ b/packages/nestjs/src/integrations/vendored/semconv.ts @@ -0,0 +1,50 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * NOTICE from the Sentry authors: + * - Vendored from: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/15ef7506553f631ea4181391e0c5725a56f0d082/packages/instrumentation-nestjs-core + * - Upstream version: @opentelemetry/instrumentation-nestjs-core@0.64.0 + */ +/* eslint-disable */ + +/* + * This file contains a copy of unstable semantic convention definitions + * used by this package. + * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv + */ + +/** + * Deprecated, use `http.request.method` instead. + * + * @example GET + * @example POST + * @example HEAD + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by `http.request.method`. + */ +export const ATTR_HTTP_METHOD = 'http.method' as const; + +/** + * Deprecated, use `url.full` instead. + * + * @example https://www.foo.bar/search?q=OpenTelemetry#SemConv + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by `url.full`. + */ +export const ATTR_HTTP_URL = 'http.url' as const; diff --git a/yarn.lock b/yarn.lock index 7337d680cc8f..56656eca24a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6146,14 +6146,6 @@ "@opentelemetry/semantic-conventions" "^1.33.0" "@types/mysql" "2.15.27" -"@opentelemetry/instrumentation-nestjs-core@0.60.0": - version "0.60.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.60.0.tgz#60a34a8a3af7e3ab4cd7e46c783c99ff2430f2fb" - integrity sha512-BZqFAoD+frnwjpb0/T4kEEQMhl2YykZch4n2MMLKAVTzTehTBBV2hZxvFF629ipS+WOGBKjCjz1dycU9QNIckQ== - dependencies: - "@opentelemetry/instrumentation" "^0.214.0" - "@opentelemetry/semantic-conventions" "^1.30.0" - "@opentelemetry/instrumentation-pg@0.66.0": version "0.66.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.66.0.tgz#78d16b50dc4c5d851015823611a46243d63a88fb"