Skip to content

Commit 21e46bc

Browse files
chargomes1gr1d
authored andcommitted
inline otel instrumentation for testing
1 parent cdf4351 commit 21e46bc

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/*
2+
* This file is based on code from the OpenTelemetry Authors
3+
* Source: https://github.com/open-telemetry/opentelemetry-js-contrib
4+
*
5+
* Modified for immediate requirements while maintaining compliance
6+
* with the original Apache 2.0 license terms.
7+
*
8+
* Original License:
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* https://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
*/
21+
22+
import * as api from '@opentelemetry/api';
23+
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
24+
import {
25+
InstrumentationBase,
26+
InstrumentationNodeModuleDefinition,
27+
InstrumentationNodeModuleFile,
28+
isWrapped,
29+
} from '@opentelemetry/instrumentation';
30+
import type { NestFactory } from '@nestjs/core/nest-factory.js';
31+
import type { RouterExecutionContext } from '@nestjs/core/router/router-execution-context.js';
32+
import type { Controller } from '@nestjs/common/interfaces';
33+
import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions';
34+
35+
import { SDK_VERSION } from '@sentry/core';
36+
37+
const supportedVersions = ['>=4.0.0 <12'];
38+
const COMPONENT = '@nestjs/core';
39+
40+
enum AttributeNames {
41+
VERSION = 'nestjs.version',
42+
TYPE = 'nestjs.type',
43+
MODULE = 'nestjs.module',
44+
CONTROLLER = 'nestjs.controller',
45+
CALLBACK = 'nestjs.callback',
46+
PIPES = 'nestjs.pipes',
47+
INTERCEPTORS = 'nestjs.interceptors',
48+
GUARDS = 'nestjs.guards',
49+
}
50+
51+
export enum NestType {
52+
APP_CREATION = 'app_creation',
53+
REQUEST_CONTEXT = 'request_context',
54+
REQUEST_HANDLER = 'handler',
55+
}
56+
57+
/**
58+
*
59+
*/
60+
export class NestInstrumentation extends InstrumentationBase {
61+
public constructor(config: InstrumentationConfig = {}) {
62+
super('sentry-nestjs', SDK_VERSION, config);
63+
}
64+
65+
/**
66+
*
67+
*/
68+
public init(): InstrumentationNodeModuleDefinition {
69+
const module = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions);
70+
71+
module.files.push(
72+
this._getNestFactoryFileInstrumentation(supportedVersions),
73+
this._getRouterExecutionContextFileInstrumentation(supportedVersions),
74+
);
75+
76+
return module;
77+
}
78+
79+
/**
80+
*
81+
*/
82+
private _getNestFactoryFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile {
83+
return new InstrumentationNodeModuleFile(
84+
'@nestjs/core/nest-factory.js',
85+
versions,
86+
// todo
87+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+
(NestFactoryStatic: any, moduleVersion?: string) => {
89+
this._ensureWrapped(
90+
// todo
91+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
92+
NestFactoryStatic.NestFactoryStatic.prototype,
93+
'create',
94+
createWrapNestFactoryCreate(this.tracer, moduleVersion),
95+
);
96+
return NestFactoryStatic;
97+
},
98+
// todo
99+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100+
(NestFactoryStatic: any) => {
101+
// todo
102+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
103+
this._unwrap(NestFactoryStatic.NestFactoryStatic.prototype, 'create');
104+
},
105+
);
106+
}
107+
108+
/**
109+
*
110+
*/
111+
private _getRouterExecutionContextFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile {
112+
return new InstrumentationNodeModuleFile(
113+
'@nestjs/core/router/router-execution-context.js',
114+
versions,
115+
// todo
116+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
117+
(RouterExecutionContext: any, moduleVersion?: string) => {
118+
this._ensureWrapped(
119+
// todo
120+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
121+
RouterExecutionContext.RouterExecutionContext.prototype,
122+
'create',
123+
createWrapCreateHandler(this.tracer, moduleVersion),
124+
);
125+
return RouterExecutionContext;
126+
},
127+
// todo
128+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
129+
(RouterExecutionContext: any) => {
130+
// todo
131+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
132+
this._unwrap(RouterExecutionContext.RouterExecutionContext.prototype, 'create');
133+
},
134+
);
135+
}
136+
137+
/**
138+
*
139+
*/
140+
// todo
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
private _ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any): void {
143+
// todo
144+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
145+
if (isWrapped(obj[methodName])) {
146+
this._unwrap(obj, methodName);
147+
}
148+
this._wrap(obj, methodName, wrapper);
149+
}
150+
}
151+
152+
function createWrapNestFactoryCreate(tracer: api.Tracer, moduleVersion?: string) {
153+
return function wrapCreate(original: typeof NestFactory.create) {
154+
return function createWithTrace(
155+
this: typeof NestFactory,
156+
// todo
157+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
158+
nestModule: any,
159+
/* serverOrOptions */
160+
) {
161+
const span = tracer.startSpan('Create Nest App', {
162+
attributes: {
163+
component: COMPONENT,
164+
[AttributeNames.TYPE]: NestType.APP_CREATION,
165+
[AttributeNames.VERSION]: moduleVersion,
166+
// todo
167+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
168+
[AttributeNames.MODULE]: nestModule.name,
169+
},
170+
});
171+
const spanContext = api.trace.setSpan(api.context.active(), span);
172+
173+
return api.context.with(spanContext, async () => {
174+
try {
175+
// todo
176+
// eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any
177+
return await original.apply(this, arguments as any);
178+
// todo
179+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
180+
} catch (e: any) {
181+
throw addError(span, e);
182+
} finally {
183+
span.end();
184+
}
185+
});
186+
};
187+
};
188+
}
189+
190+
function createWrapCreateHandler(tracer: api.Tracer, moduleVersion?: string) {
191+
return function wrapCreateHandler(original: RouterExecutionContext['create']) {
192+
return function createHandlerWithTrace(
193+
this: RouterExecutionContext,
194+
instance: Controller,
195+
// todo
196+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
197+
callback: (...args: any[]) => unknown,
198+
) {
199+
// todo
200+
// eslint-disable-next-line prefer-rest-params
201+
arguments[1] = createWrapHandler(tracer, moduleVersion, callback);
202+
// todo
203+
// eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any
204+
const handler = original.apply(this, arguments as any);
205+
const callbackName = callback.name;
206+
const instanceName =
207+
// todo
208+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
209+
instance.constructor && instance.constructor.name ? instance.constructor.name : 'UnnamedInstance';
210+
const spanName = callbackName ? `${instanceName}.${callbackName}` : instanceName;
211+
212+
// todo
213+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
214+
return function (this: any, req: any, res: any, next: (...args: any[]) => unknown) {
215+
const span = tracer.startSpan(spanName, {
216+
attributes: {
217+
component: COMPONENT,
218+
[AttributeNames.VERSION]: moduleVersion,
219+
[AttributeNames.TYPE]: NestType.REQUEST_CONTEXT,
220+
// todo
221+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
222+
[ATTR_HTTP_REQUEST_METHOD]: req.method,
223+
// todo
224+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, deprecation/deprecation
225+
[SEMATTRS_HTTP_URL]: req.originalUrl || req.url,
226+
// todo
227+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
228+
[ATTR_HTTP_ROUTE]: req.route?.path || req.routeOptions?.url || req.routerPath,
229+
[AttributeNames.CONTROLLER]: instanceName,
230+
[AttributeNames.CALLBACK]: callbackName,
231+
},
232+
});
233+
const spanContext = api.trace.setSpan(api.context.active(), span);
234+
235+
return api.context.with(spanContext, async () => {
236+
try {
237+
// todo
238+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, prefer-rest-params
239+
return await handler.apply(this, arguments as unknown);
240+
// todo
241+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
242+
} catch (e: any) {
243+
throw addError(span, e);
244+
} finally {
245+
span.end();
246+
}
247+
});
248+
};
249+
};
250+
};
251+
}
252+
253+
function createWrapHandler(
254+
tracer: api.Tracer,
255+
moduleVersion: string | undefined,
256+
// todo
257+
// eslint-disable-next-line @typescript-eslint/ban-types
258+
handler: Function,
259+
// todo
260+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
261+
): (this: RouterExecutionContext) => Promise<any> {
262+
const spanName = handler.name || 'anonymous nest handler';
263+
const options = {
264+
attributes: {
265+
component: COMPONENT,
266+
[AttributeNames.VERSION]: moduleVersion,
267+
[AttributeNames.TYPE]: NestType.REQUEST_HANDLER,
268+
[AttributeNames.CALLBACK]: handler.name,
269+
},
270+
};
271+
// todo
272+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
273+
const wrappedHandler = function (this: RouterExecutionContext): Promise<any> {
274+
const span = tracer.startSpan(spanName, options);
275+
const spanContext = api.trace.setSpan(api.context.active(), span);
276+
277+
return api.context.with(spanContext, async () => {
278+
try {
279+
// todo
280+
// eslint-disable-next-line prefer-rest-params
281+
return await handler.apply(this, arguments);
282+
// todo
283+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
284+
} catch (e: any) {
285+
throw addError(span, e);
286+
} finally {
287+
span.end();
288+
}
289+
});
290+
};
291+
292+
if (handler.name) {
293+
Object.defineProperty(wrappedHandler, 'name', { value: handler.name });
294+
}
295+
296+
// Get the current metadata and set onto the wrapper to ensure other decorators ( ie: NestJS EventPattern / RolesGuard )
297+
// won't be affected by the use of this instrumentation
298+
Reflect.getMetadataKeys(handler).forEach(metadataKey => {
299+
Reflect.defineMetadata(metadataKey, Reflect.getMetadata(metadataKey, handler), wrappedHandler);
300+
});
301+
return wrappedHandler;
302+
}
303+
304+
const addError = (span: api.Span, error: Error): Error => {
305+
span.recordException(error);
306+
span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
307+
return error;
308+
};

0 commit comments

Comments
 (0)