diff --git a/.oxlintrc.base.json b/.oxlintrc.base.json index abc918050629..e0e82b2c2457 100644 --- a/.oxlintrc.base.json +++ b/.oxlintrc.base.json @@ -147,6 +147,7 @@ "**/integrations/fs/vendored/**/*.ts", "**/integrations/tracing/knex/vendored/**/*.ts", "**/integrations/tracing/mongo/vendored/**/*.ts", + "**/integrations/tracing/koa/vendored/**/*.ts", "**/integrations/tracing/connect/vendored/**/*.ts", "**/integration/aws/vendored/**/*.ts", "**/nestjs/src/integrations/vendored/**/*.ts", diff --git a/packages/node/package.json b/packages/node/package.json index 636640cd5bbc..a3a81ba750e6 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -72,7 +72,6 @@ "@opentelemetry/instrumentation-graphql": "0.62.0", "@opentelemetry/instrumentation-hapi": "0.60.0", "@opentelemetry/instrumentation-http": "0.214.0", - "@opentelemetry/instrumentation-koa": "0.62.0", "@opentelemetry/instrumentation-mongoose": "0.60.0", "@opentelemetry/instrumentation-mysql": "0.60.0", "@opentelemetry/instrumentation-mysql2": "0.60.0", diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa/index.ts similarity index 94% rename from packages/node/src/integrations/tracing/koa.ts rename to packages/node/src/integrations/tracing/koa/index.ts index 7125479e91e0..11d8e01bf669 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa/index.ts @@ -1,5 +1,5 @@ -import type { KoaInstrumentationConfig, KoaLayerType } from '@opentelemetry/instrumentation-koa'; -import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import type { KoaInstrumentationConfig, KoaLayerType } from './vendored/types'; +import { KoaInstrumentation } from './vendored/instrumentation'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import type { IntegrationFn } from '@sentry/core'; import { @@ -12,7 +12,7 @@ import { spanToJSON, } from '@sentry/core'; import { addOriginToSpan, ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core'; -import { DEBUG_BUILD } from '../../debug-build'; +import { DEBUG_BUILD } from '../../../debug-build'; interface KoaOptions { /** diff --git a/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts new file mode 100644 index 000000000000..28dd9a00fed2 --- /dev/null +++ b/packages/node/src/integrations/tracing/koa/vendored/enums/AttributeNames.ts @@ -0,0 +1,25 @@ +/* + * 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-koa + * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 + */ +/* eslint-disable */ + +export enum AttributeNames { + KOA_TYPE = 'koa.type', + KOA_NAME = 'koa.name', +} diff --git a/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts b/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts new file mode 100644 index 000000000000..6251e4d81947 --- /dev/null +++ b/packages/node/src/integrations/tracing/koa/vendored/instrumentation.ts @@ -0,0 +1,196 @@ +/* + * 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-koa + * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 + * - Minor TypeScript strictness adjustments for this repository's compiler settings + */ +/* eslint-disable */ + +import * as api from '@opentelemetry/api'; +import { + isWrapped, + InstrumentationBase, + InstrumentationNodeModuleDefinition, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; + +import { KoaLayerType, KoaInstrumentationConfig } from './types'; +import { SDK_VERSION } from '@sentry/core'; +import { getMiddlewareMetadata, isLayerIgnored } from './utils'; +import { getRPCMetadata, RPCType } from '@opentelemetry/core'; +import { Next, kLayerPatched, KoaContext, KoaMiddleware, KoaPatchedMiddleware } from './internal-types'; + +const PACKAGE_NAME = '@sentry/instrumentation-koa'; + +/** Koa instrumentation for OpenTelemetry */ +export class KoaInstrumentation extends InstrumentationBase { + constructor(config: KoaInstrumentationConfig = {}) { + super(PACKAGE_NAME, SDK_VERSION, config); + } + + protected init() { + return new InstrumentationNodeModuleDefinition( + 'koa', + ['>=2.0.0 <4'], + (module: any) => { + const moduleExports: any = + module[Symbol.toStringTag] === 'Module' + ? module.default // ESM + : module; // CommonJS + if (moduleExports == null) { + return moduleExports; + } + if (isWrapped(moduleExports.prototype.use)) { + this._unwrap(moduleExports.prototype, 'use'); + } + this._wrap(moduleExports.prototype, 'use', this._getKoaUsePatch.bind(this)); + return module; + }, + (module: any) => { + const moduleExports: any = + module[Symbol.toStringTag] === 'Module' + ? module.default // ESM + : module; // CommonJS + if (isWrapped(moduleExports.prototype.use)) { + this._unwrap(moduleExports.prototype, 'use'); + } + }, + ); + } + + /** + * Patches the Koa.use function in order to instrument each original + * middleware layer which is introduced + * @param {KoaMiddleware} middleware - the original middleware function + */ + private _getKoaUsePatch(original: (middleware: KoaMiddleware) => any) { + const plugin = this; + return function use(this: any, middlewareFunction: KoaMiddleware) { + let patchedFunction: KoaMiddleware; + if (middlewareFunction.router) { + patchedFunction = plugin._patchRouterDispatch(middlewareFunction); + } else { + patchedFunction = plugin._patchLayer(middlewareFunction, false); + } + return original.apply(this, [patchedFunction]); + }; + } + + /** + * Patches the dispatch function used by @koa/router. This function + * goes through each routed middleware and adds instrumentation via a call + * to the @function _patchLayer function. + * @param {KoaMiddleware} dispatchLayer - the original dispatch function which dispatches + * routed middleware + */ + private _patchRouterDispatch(dispatchLayer: KoaMiddleware): KoaMiddleware { + api.diag.debug('Patching @koa/router dispatch'); + + const router = dispatchLayer.router; + + const routesStack = router?.stack ?? []; + for (const pathLayer of routesStack) { + const path = pathLayer.path; + // Type cast needed: router.stack comes from @types/koa@2.x but we use @types/koa@3.x + // See internal-types.ts for full explanation + const pathStack = pathLayer.stack as any; + for (let j = 0; j < pathStack.length; j++) { + const routedMiddleware: KoaMiddleware = pathStack[j]; + pathStack[j] = this._patchLayer(routedMiddleware, true, path); + } + } + + return dispatchLayer; + } + + /** + * Patches each individual @param middlewareLayer function in order to create the + * span and propagate context. It does not create spans when there is no parent span. + * @param {KoaMiddleware} middlewareLayer - the original middleware function. + * @param {boolean} isRouter - tracks whether the original middleware function + * was dispatched by the router originally + * @param {string?} layerPath - if present, provides additional data from the + * router about the routed path which the middleware is attached to + */ + private _patchLayer( + middlewareLayer: KoaPatchedMiddleware, + isRouter: boolean, + layerPath?: string | RegExp, + ): KoaMiddleware { + const layerType = isRouter ? KoaLayerType.ROUTER : KoaLayerType.MIDDLEWARE; + // Skip patching layer if its ignored in the config + if (middlewareLayer[kLayerPatched] === true || isLayerIgnored(layerType, this.getConfig())) return middlewareLayer; + + if ( + middlewareLayer.constructor.name === 'GeneratorFunction' || + middlewareLayer.constructor.name === 'AsyncGeneratorFunction' + ) { + api.diag.debug('ignoring generator-based Koa middleware layer'); + return middlewareLayer; + } + + middlewareLayer[kLayerPatched] = true; + + api.diag.debug('patching Koa middleware layer'); + return async (context: KoaContext, next: Next) => { + const parent = api.trace.getSpan(api.context.active()); + if (parent === undefined) { + return middlewareLayer(context, next); + } + const metadata = getMiddlewareMetadata(context, middlewareLayer, isRouter, layerPath); + const span = this.tracer.startSpan(metadata.name, { + attributes: metadata.attributes, + }); + + const rpcMetadata = getRPCMetadata(api.context.active()); + + if (rpcMetadata?.type === RPCType.HTTP && context._matchedRoute) { + rpcMetadata.route = context._matchedRoute.toString(); + } + + const { requestHook } = this.getConfig(); + if (requestHook) { + safeExecuteInTheMiddle( + () => + requestHook(span, { + context, + middlewareLayer, + layerType, + }), + e => { + if (e) { + api.diag.error('koa instrumentation: request hook failed', e); + } + }, + true, + ); + } + + const newContext = api.trace.setSpan(api.context.active(), span); + return api.context.with(newContext, async () => { + try { + return await middlewareLayer(context, next); + } catch (err: any) { + span.recordException(err); + throw err; + } finally { + span.end(); + } + }); + }; + } +} diff --git a/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts b/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts new file mode 100644 index 000000000000..a2039dc009cb --- /dev/null +++ b/packages/node/src/integrations/tracing/koa/vendored/internal-types.ts @@ -0,0 +1,65 @@ +/* + * 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-koa + * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 + * - Some types vendored from @types/koa, @types/koa-compose, and @types/koa__router with simplifications + */ +/* eslint-disable */ + +interface DefaultState {} + +export type Next = () => Promise; + +type ParameterizedContext<_StateT = DefaultState, ContextT = {}, _ResponseBodyT = unknown> = { + [key: string]: any; +} & ContextT; + +type Middleware = ( + context: ParameterizedContext, + next: Next, +) => any; + +interface RouterParamContext { + params: Record; + router: Router; + _matchedRoute: string | RegExp | undefined; + _matchedRouteName: string | undefined; +} + +interface Layer { + path: string | RegExp; + stack: any[]; +} + +export interface Router<_StateT = DefaultState, _ContextT = {}> { + stack: Layer[]; +} + +export type KoaContext = ParameterizedContext; +export type KoaMiddleware = Middleware & { + router?: Router; +}; + +/** + * This symbol is used to mark a Koa layer as being already instrumented + * since its possible to use a given layer multiple times (ex: middlewares) + */ +export const kLayerPatched: unique symbol = Symbol('koa-layer-patched'); + +export type KoaPatchedMiddleware = KoaMiddleware & { + [kLayerPatched]?: boolean; +}; diff --git a/packages/node/src/integrations/tracing/koa/vendored/types.ts b/packages/node/src/integrations/tracing/koa/vendored/types.ts new file mode 100644 index 000000000000..54a79d68e252 --- /dev/null +++ b/packages/node/src/integrations/tracing/koa/vendored/types.ts @@ -0,0 +1,74 @@ +/* + * 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-koa + * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 + */ +/* eslint-disable */ + +import { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export enum KoaLayerType { + ROUTER = 'router', + MIDDLEWARE = 'middleware', +} + +/** + * Information about the current Koa middleware layer + * The middleware layer type is any by default. + * One can install koa types packages `@types/koa` and `@types/koa__router` + * with compatible versions to the koa version used in the project + * to get more specific types for the middleware layer property. + * + * Example use in a custom attribute function: + * ```ts + * import type { Middleware, ParameterizedContext, DefaultState } from 'koa'; + * import type { RouterParamContext } from '@koa/router'; + * + * type KoaContext = ParameterizedContext; + * type KoaMiddleware = Middleware; + * + * const koaConfig: KoaInstrumentationConfig = { + * requestHook: (span: Span, info: KoaRequestInfo) => { + * // custom typescript code that can access the typed into.middlewareLayer and info.context + * } + * + */ +export type KoaRequestInfo = { + context: KoaContextType; + middlewareLayer: KoaMiddlewareType; + layerType: KoaLayerType; +}; + +/** + * Function that can be used to add custom attributes to the current span + * @param span - The Express middleware layer span. + * @param context - The current KoaContext. + */ +export interface KoaRequestCustomAttributeFunction { + (span: Span, info: KoaRequestInfo): void; +} + +/** + * Options available for the Koa Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-Instrumentation-koa#koa-Instrumentation-options)) + */ +export interface KoaInstrumentationConfig extends InstrumentationConfig { + /** Ignore specific layers based on their type */ + ignoreLayersType?: KoaLayerType[]; + /** Function for adding custom attributes to each middleware layer span */ + requestHook?: KoaRequestCustomAttributeFunction; +} diff --git a/packages/node/src/integrations/tracing/koa/vendored/utils.ts b/packages/node/src/integrations/tracing/koa/vendored/utils.ts new file mode 100644 index 000000000000..2066afde33c5 --- /dev/null +++ b/packages/node/src/integrations/tracing/koa/vendored/utils.ts @@ -0,0 +1,65 @@ +/* + * 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-koa + * - Upstream version: @opentelemetry/instrumentation-koa@0.66.0 + */ +/* eslint-disable */ + +import { KoaLayerType, KoaInstrumentationConfig } from './types'; +import { KoaContext, KoaMiddleware } from './internal-types'; +import { AttributeNames } from './enums/AttributeNames'; +import { Attributes } from '@opentelemetry/api'; +import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; + +export const getMiddlewareMetadata = ( + context: KoaContext, + layer: KoaMiddleware, + isRouter: boolean, + layerPath?: string | RegExp, +): { + attributes: Attributes; + name: string; +} => { + if (isRouter) { + return { + attributes: { + [AttributeNames.KOA_NAME]: layerPath?.toString(), + [AttributeNames.KOA_TYPE]: KoaLayerType.ROUTER, + [ATTR_HTTP_ROUTE]: layerPath?.toString(), + }, + name: context._matchedRouteName || `router - ${layerPath}`, + }; + } else { + return { + attributes: { + [AttributeNames.KOA_NAME]: layer.name ?? 'middleware', + [AttributeNames.KOA_TYPE]: KoaLayerType.MIDDLEWARE, + }, + name: `middleware - ${layer.name}`, + }; + } +}; + +/** + * Check whether the given request is ignored by configuration + * @param [list] List of ignore patterns + * @param [onException] callback for doing something when an exception has + * occurred + */ +export const isLayerIgnored = (type: KoaLayerType, config?: KoaInstrumentationConfig): boolean => { + return !!(Array.isArray(config?.ignoreLayersType) && config?.ignoreLayersType?.includes(type)); +}; diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts index 74d10c344d95..4961b0c9b82c 100644 --- a/packages/node/test/integrations/tracing/koa.test.ts +++ b/packages/node/test/integrations/tracing/koa.test.ts @@ -1,9 +1,9 @@ -import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import { KoaInstrumentation } from '../../../src/integrations/tracing/koa/vendored/instrumentation'; import { INSTRUMENTED } from '@sentry/node-core'; import { beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa'; -vi.mock('@opentelemetry/instrumentation-koa'); +vi.mock('../../../src/integrations/tracing/koa/vendored/instrumentation'); describe('Koa', () => { beforeEach(() => { diff --git a/yarn.lock b/yarn.lock index e01782ad739e..08a987e9cf88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6093,15 +6093,6 @@ "@opentelemetry/semantic-conventions" "^1.29.0" forwarded-parse "2.1.2" -"@opentelemetry/instrumentation-koa@0.62.0": - version "0.62.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.62.0.tgz#65fdf96c1b1ffb382167cd3b7a244631afd0cc1f" - integrity sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA== - dependencies: - "@opentelemetry/core" "^2.0.0" - "@opentelemetry/instrumentation" "^0.214.0" - "@opentelemetry/semantic-conventions" "^1.36.0" - "@opentelemetry/instrumentation-mongoose@0.60.0": version "0.60.0" resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.60.0.tgz#9481a90d3f75d66244d7f63709529cb7f2823103" @@ -6224,7 +6215,7 @@ "@opentelemetry/resources" "2.6.1" "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.36.0", "@opentelemetry/semantic-conventions@^1.40.0": +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.33.0", "@opentelemetry/semantic-conventions@^1.34.0", "@opentelemetry/semantic-conventions@^1.40.0": version "1.40.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz#10b2944ca559386590683392022a897eefd011d3" integrity sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==