├── .eslintrc.js
├── .github
├── ISSUE_TEMPLATE
│ └── config.yml
└── workflows
│ ├── ci.yml
│ ├── release.yml
│ └── releaseBeta.yml
├── .gitignore
├── .prettierrc
├── README.md
├── docs
├── log.png
└── trace-flow.jpeg
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── Constants.ts
├── MetaScanner.spec.ts
├── MetaScanner.ts
├── OpenTelemetryModule.ts
├── OpenTelemetryModuleAsyncOption.ts
├── OpenTelemetryModuleConfig.interface.ts
├── OpenTelemetryModuleConfigDefault.ts
├── Trace
│ ├── Decorators
│ │ ├── Span.ts
│ │ └── Traceable.ts
│ ├── Injectors
│ │ ├── BaseTraceInjector.spec.ts
│ │ ├── BaseTraceInjector.ts
│ │ ├── ConsoleLoggerInjector.ts
│ │ ├── ControllerInjector.spec.ts
│ │ ├── ControllerInjector.ts
│ │ ├── DecoratorInjector.spec.ts
│ │ ├── DecoratorInjector.ts
│ │ ├── EventEmitterInjector.spec.ts
│ │ ├── EventEmitterInjector.ts
│ │ ├── GraphQLResolverInjector.spec.ts
│ │ ├── GraphQLResolverInjector.ts
│ │ ├── GuardInjector.spec.ts
│ │ ├── GuardInjector.ts
│ │ ├── Injector.ts
│ │ ├── PipeInjector.spec.ts
│ │ ├── PipeInjector.ts
│ │ ├── ScheduleInjector.ts
│ │ └── SchedulerInjector.spec.ts
│ ├── Logger.interface.ts
│ ├── NoopTraceExporter.ts
│ ├── TraceService.ts
│ ├── TraceWrapper.spec.ts
│ ├── TraceWrapper.ts
│ └── TraceWrapper.types.ts
├── Tracing.ts
├── TracingConfig.interface.ts
├── TracingConfigDefault.ts
└── index.ts
├── tsconfig.build.json
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: No issues undertaking
4 | url: https://github.com/amplication/amplication/issues/new/choose
5 | about: We are not tracking issues in this repo. To report an issue related to this repo, please open the issue on our main repo amplication/amplication
6 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches-ignore: ['main']
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: lts/*
17 | cache: npm
18 | - run: npm ci
19 | - run: npm run lint
20 | - run: npm run test:cov
21 | - run: npm run build
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to npmjs
2 | on:
3 | push:
4 | branches: [main]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | contents: write # to be able to publish a GitHub release
10 | issues: write # to be able to comment on released issues
11 | pull-requests: write # to be able to comment on released pull requests
12 | id-token: write # to enable use of OIDC for npm provenance
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v3
16 | with:
17 | fetch-depth: 0
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 'lts/*'
22 | - name: Install dependencies
23 | run: npm ci
24 | - run: npm run lint
25 | - run: npm run test:cov
26 | - run: npm run build
27 | - name: Release
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31 | run: npx semantic-release
32 |
--------------------------------------------------------------------------------
/.github/workflows/releaseBeta.yml:
--------------------------------------------------------------------------------
1 | name: Manual Publish Package
2 | run-name: Version ${{ github.event.inputs.version }}
3 |
4 | on:
5 | workflow_dispatch:
6 | inputs:
7 | version:
8 | type: string
9 | required: true
10 | description: 'The version to publish. Must be a valid semver version. i.e. 1.0.0-beta.1'
11 | jobs:
12 | manual-release:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 | - name: Setup Node.js
20 | uses: actions/setup-node@v3
21 | with:
22 | node-version: 'lts/*'
23 | - name: Install
24 | run: npm ci
25 | - name: Set version
26 | run: npm version ${{ inputs.version }} --no-git-tag-version
27 | - name: Release
28 | run: npm publish --access public --tag beta
29 | env:
30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /node_modules
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 |
12 | # OS
13 | .DS_Store
14 |
15 | # Tests
16 | /coverage
17 | /.nyc_output
18 |
19 | # IDEs and editors
20 | /.idea
21 | .project
22 | .classpath
23 | .c9/
24 | *.launch
25 | .settings/
26 | *.sublime-workspace
27 |
28 | # IDE - VSCode
29 | .vscode/*
30 | !.vscode/settings.json
31 | !.vscode/tasks.json
32 | !.vscode/launch.json
33 | !.vscode/extensions.json
34 | dist
35 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NestJS OpenTelemetry
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | This library, initially forked from [@overbit/opentelemetry-nestjs](https://github.com/overbit/opentelemetry-nestjs.git), provides deeply integrated protocol-agnostic Nestjs [OpenTelemetry](https://opentelemetry.io/) instrumentations, metrics and SDK.
10 |
11 | ## Description
12 |
13 | Nestjs is a protocol-agnostic framework. That's why this library can able to work with different protocols like RabbitMQ, GRPC and HTTP. Also you can observe and trace Nestjs specific layers like [Pipe](https://docs.nestjs.com/pipes), [Guard](https://docs.nestjs.com/guards), [Controller](https://docs.nestjs.com/controllers) and [Provider](https://docs.nestjs.com/providers).
14 |
15 | It also includes auto trace and metric instrumentations for some popular Nestjs libraries.
16 |
17 | - ### Distributed Tracing
18 | - [Setup](#distributed-tracing-1)
19 | - [Decorators](#trace-decorators)
20 | - [Trace Providers](#trace-providers)
21 | - [Trace Not @Injectable() classes](#trace-not-injectable-classes)
22 | - [Auto Trace Instrumentations](#auto-trace-instrumentations)
23 | - [Distributed Logging with Trace ID](#distributed-logging-with-trace-id)
24 | - ### Metrics
25 | - [Setup](#metrics-1)
26 |
27 | OpenTelemetry Metrics currently experimental. So, this library doesn't support metric decorators and Auto Observers until it's stable. but if you want to use it, you can use OpenTelemetry API directly.
28 |
29 | Competability table for Nestjs versions.
30 |
31 | | Nestjs | Nestjs-OpenTelemetry |
32 | | ------ | -------------------- |
33 | | 9.x | 3.x.x |
34 | | 8.x | 2.x.x |
35 |
36 | ## Installation
37 |
38 | ```bash
39 | npm install @amplication/opentelemetry-nestjs --save
40 | ```
41 |
42 | ---
43 |
44 | ## Configuration
45 |
46 | This is a basic configuration without any trace and metric exporter, but includes default metrics and injectors
47 |
48 | ```ts
49 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
50 |
51 | @Module({
52 | imports: [
53 | OpenTelemetryModule.forRoot({
54 | serviceName: 'nestjs-opentelemetry-example',
55 | }),
56 | ],
57 | })
58 | export class AppModule {}
59 | ```
60 |
61 | Async configuration example
62 |
63 | ```ts
64 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
65 | import { ConfigModule, ConfigService } from '@nestjs/config';
66 |
67 | @Module({
68 | imports: [
69 | OpenTelemetryModule.forRootAsync({
70 | imports: [ConfigModule],
71 | useFactory: async (configService: ConfigService) => ({
72 | serviceName: configService.get('SERVICE_NAME'),
73 | }),
74 | inject: [ConfigService],
75 | }),
76 | ],
77 | })
78 | export class AppModule {}
79 | ```
80 |
81 | ### Configuration types
82 |
83 | `Tracing.init()` takes [TracingConfig](https://github.com/amplication/opentelemetry-nestjs/blob/main/src/TracingConfig.interface.ts#L3) as a parameter, this type is inherited by [NodeSDKConfiguration](https://github.com/open-telemetry/opentelemetry-js/blob/745bd5c34d3961dc73873190adc763747e5e026d/experimental/packages/opentelemetry-sdk-node/src/types.ts#:~:text=NodeSDKConfiguration) so you can use same OpenTelemetry SDK parameter.
84 |
85 | `OpenTelemetryModule.forRoot()` takes [OpenTelemetryModuleConfig](https://github.com/amplication/opentelemetry-nestjs/blob/main/src/OpenTelemetryModuleConfig.interface.ts#L5)
86 |
87 | ### Default Parameters
88 |
89 |
90 |
91 |
92 | key
93 | value
94 | description
95 |
96 |
97 |
98 | traceAutoInjectors
99 |
100 | ControllerInjector, GuardInjector, EventEmitterInjector, ScheduleInjector, PipeInjector, LoggerInjector
101 | default auto trace instrumentations inherited from NodeSDKConfiguration
102 |
103 |
104 | contextManager
105 | AsyncLocalStorageContextManager
106 |
107 | default trace context manager inherited from NodeSDKConfiguration
108 |
109 |
110 |
111 | instrumentations
112 | AutoInstrumentations
113 |
114 | default instrumentations inherited from defaults of@opentelemetry/auto-instrumentations-node where:
115 |
116 | @opentelemetry/instrumentation-dns
and @opentelemetry/instrumentation-net
have been disabled to reduce noise
117 | @opentelemetry/instrumentation-http
ignores common health check endpoints and creates span with name "HTTP_METHOD PATH"
118 | @opentelemetry/instrumentation-fs
ignores operations on files under node_modules
119 | @opentelemetry/instrumentation-express
has been disabled to reduce noise
120 | @opentelemetry/instrumentation-graphql
has been configured to fit with nestjs (mergeItems: true, ignoreResolveSpans: true, ignoreTrivialResolveSpans: true)
121 | @opentelemetry/instrumentation-nestjs-core
has been disabled to reduce noise being redundant
122 |
123 |
124 | spanProcessor
125 | NoopSpanProcessor
126 | default spanProcessor inherited from NodeSDKConfiguration
127 |
128 |
129 | textMapPropagator
130 | JaegerPropagator, B3Propagator
131 | default textMapPropagator inherited from NodeSDKConfiguration
132 |
133 |
134 |
135 | ---
136 |
137 | ## Distributed Tracing
138 |
139 | Simple setup with Otel exporter, including with default trace instrumentations.
140 |
141 | The setup consists of two main changes in the `main.ts` (to initialise the provider) and in the nestjs app module.
142 |
143 | ```ts
144 | // main.ts
145 | // at the very top of the file
146 | import { Tracing } from '@amplication/opentelemetry-nestjs';
147 | import { ZipkinExporter } from '@opentelemetry/exporter-zipkin';
148 | import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
149 |
150 | Tracing.init({
151 | serviceName: 'my-service',
152 | spanProcessor: new SimpleSpanProcessor(
153 | new ZipkinExporter({
154 | url: 'your-zipkin-url',
155 | }),
156 | ),
157 | });
158 |
159 | import { NestFactory } from '@nestjs/core';
160 |
161 | // ....
162 | ```
163 |
164 | ```ts
165 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
166 |
167 | @Module({
168 | imports: [OpenTelemetryModule.forRoot()],
169 | })
170 | export class AppModule {}
171 | ```
172 |
173 | After setup, your application will be instrumented, so that you can see almost every layer of application in ZipkinUI, including Guards, Pipes, Controllers even global layers like this
174 |
175 | 
176 |
177 | List of supported official exporters [here](https://opentelemetry.io/docs/js/exporters/).
178 |
179 | ---
180 |
181 | ### Trace Decorators
182 |
183 | This library supports auto instrumentations for Nestjs layers, but sometimes you need to define custom span for specific method blocks like providers methods. In this case `@Traceable` and `@Span` decorators will help you.
184 |
185 | #### `@Span`
186 |
187 | ```ts
188 | import { Injectable } from '@nestjs/common';
189 | import { Span } from '@amplication/opentelemetry-nestjs';
190 |
191 | @Injectable()
192 | export class AppService {
193 | @Span()
194 | getHello(): string {
195 | return 'Hello World!';
196 | }
197 | }
198 | ```
199 |
200 | Also `@Span` decorator takes `name` field as a parameter
201 |
202 | ```ts
203 | @Span('hello')
204 | ```
205 |
206 | #### `@Traceable`
207 |
208 | `@Traceable` works like `@Span` but with the difference that it can be used at a class level to auto instrument every method of the class
209 |
210 | ```ts
211 | import { Injectable } from '@nestjs/common';
212 | import { Traceable } from '@amplication/opentelemetry-nestjs';
213 |
214 | @Traceable()
215 | @Injectable()
216 | export class AppService {
217 | getHello(): string {
218 | return 'Hello World!';
219 | }
220 | }
221 | ```
222 |
223 | ---
224 |
225 | ### Trace Providers
226 |
227 | In an advanced use cases, you need to access the native OpenTelemetry Trace provider to access them from Nestjs application context.
228 |
229 | ```ts
230 | import { Injectable } from '@nestjs/common';
231 | import { Tracer } from '@opentelemetry/sdk-trace-node';
232 |
233 | @Injectable()
234 | export class AppService {
235 | constructor(private readonly tracer: Tracer) {}
236 |
237 | getHello(): string {
238 | const span = this.tracer.startSpan('important_section_start');
239 | // do something important
240 | span.setAttributes({ userId: 1150 });
241 | span.end();
242 | return 'Hello World!';
243 | }
244 | }
245 | ```
246 |
247 | `TraceService` can access directly current span context and start new span.
248 |
249 | ```ts
250 | import { Injectable } from '@nestjs/common';
251 | import { TraceService } from '@amplication/opentelemetry-nestjs';
252 |
253 | @Injectable()
254 | export class AppService {
255 | constructor(private readonly traceService: TraceService) {}
256 |
257 | getHello(): string {
258 | const span = this.traceService.startSpan('hello');
259 | // do something
260 | span.end();
261 | return 'Hello World!';
262 | }
263 | }
264 | ```
265 |
266 | ---
267 |
268 | ### Auto Trace Instrumentations
269 |
270 | The most helpful part of this library is that you already get all of the instrumentations by default if you set up a module without any extra configuration. If you need to avoid some of them, you can use the `traceAutoInjectors` parameter.
271 |
272 | ```ts
273 | import { Module } from '@nestjs/common';
274 | import {
275 | OpenTelemetryModule,
276 | ControllerInjector,
277 | EventEmitterInjector,
278 | GuardInjector,
279 | LoggerInjector,
280 | PipeInjector,
281 | ScheduleInjector,
282 | } from '@amplication/opentelemetry-nestjs';
283 |
284 | @Module({
285 | imports: [
286 | OpenTelemetryModule.forRoot([
287 | ControllerInjector,
288 | GuardInjector,
289 | EventEmitterInjector,
290 | ScheduleInjector,
291 | PipeInjector,
292 | LoggerInjector,
293 | ]),
294 | ],
295 | })
296 | export class AppModule {}
297 | ```
298 |
299 | #### List of Trace Injectors
300 |
301 | | Instance | Description |
302 | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
303 | | `ControllerInjector` | Auto trace all of module controllers |
304 | | `GuardInjector` | Auto trace all of module guards including global guards |
305 | | `PipeInjector` | Auto trace all of module pipes including global pipes |
306 | | `EventEmitterInjector` | Auto trace for [@nestjs/event-emitter](https://docs.nestjs.com/techniques/events) library, supports all features |
307 | | `ScheduleInjector` | Auto trace for [@nestjs/schedule](https://docs.nestjs.com/techniques/task-scheduling) library, supports all features |
308 | | `ConsoleLoggerInjector` | [ConsoleLogger](https://docs.nestjs.com/techniques/logger#extend-built-in-logger) and [Logger](https://docs.nestjs.com/techniques/logger#using-the-logger-for-application-logging) class tracer, logs with traceId |
309 |
310 | ---
311 |
312 | #### Distributed Logging with Trace ID
313 |
314 | When you set up your environment with the `LoggerInjector` class or default configuration, you can see trace id with every log.
315 |
316 | 
317 |
318 | ---
319 |
320 | ### Trace Not @Injectable() classes
321 |
322 | In some use cases, you need to trace instances of classes instanciated outside the NestJS DI container.
323 | In order to do so, use the `TraceWrapper.trace()` method to wrap every method of the instance in a new span as follow
324 |
325 | ```ts
326 | import { TraceWrapper } from '@amplication/opentelemetry-nestjs';
327 |
328 | class MyClass {
329 | hello() {
330 | console.log('Hi');
331 | }
332 |
333 | async bye() {
334 | await new Promise(() => console.log('bye bye'));
335 | }
336 | }
337 |
338 | // ....
339 | const instance = new MyClass();
340 | const tracedInstance = TraceWrapper.trace(instance);
341 |
342 | // ....
343 | ```
344 |
345 | ## Metrics
346 |
347 | Simple setup with Prometheus exporter, you need install [@opentelemetry/exporter-prometheus](https://www.npmjs.com/package/@opentelemetry/exporter-prometheus)
348 |
349 | ```ts
350 | // main.ts
351 | // at the very top of the file
352 | import { Tracing } from '@amplication/opentelemetry-nestjs';
353 | import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
354 |
355 | Tracing.init({
356 | serviceName: 'nestjs-opentelemetry-example',
357 | metricReader: new PrometheusExporter({
358 | endpoint: 'metrics',
359 | port: 9464,
360 | }),
361 | });
362 |
363 | import { NestFactory } from '@nestjs/core';
364 | // ....
365 | ```
366 |
367 | Now you can access Prometheus exporter with auto collected metrics [http://localhost:9464/metrics](http://localhost:9464/metrics).
368 | Also, you can find different exporters [here](https://opentelemetry.io/docs/js/exporters/)
369 |
370 | ---
371 |
372 | ## Examples
373 |
374 | ```ts
375 | // main.ts
376 | // at the very top of the file
377 | import { Tracing } from '@amplication/opentelemetry-nestjs';
378 | import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
379 | import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
380 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
381 | import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
382 | import { CompositePropagator } from '@opentelemetry/core';
383 | import { JaegerPropagator } from '@opentelemetry/propagator-jaeger';
384 | import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3';
385 |
386 | Tracing.init({
387 | serviceName: 'myservice-opentelemetry-example',
388 | metricReader: new PrometheusExporter({
389 | endpoint: 'metrics',
390 | port: 9464,
391 | }),
392 | spanProcessor: new BatchSpanProcessor(
393 | new OTLPTraceExporter({
394 | url: 'your-jaeger-url',
395 | }),
396 | ),
397 | textMapPropagator: new CompositePropagator({
398 | propagators: [
399 | new JaegerPropagator(),
400 | new B3Propagator(),
401 | new B3Propagator({
402 | injectEncoding: B3InjectEncoding.MULTI_HEADER,
403 | }),
404 | ],
405 | }),
406 | });
407 |
408 | import { NestFactory } from '@nestjs/core';
409 | // ....
410 | ```
411 |
412 | ```ts
413 | // ... app.module.ts
414 | import { Module } from '@nestjs/common';
415 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
416 |
417 | @Module({
418 | imports: [OpenTelemetryModule.forRoot()],
419 | })
420 | export class AppModule {}
421 | ```
422 |
423 | ### AWS XRay
424 |
425 | For the integration with AWS X-Ray, follow the official instructions.
426 |
427 | i.e.
428 |
429 | ```ts
430 | // main.ts
431 | // at the very top of the file
432 | import { Tracing } from '@amplication/opentelemetry-nestjs';
433 | import { PrometheusExporter } from '@opentelemetry/exporter-prometheus';
434 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
435 | import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
436 | import { CompositePropagator } from '@opentelemetry/core';
437 | import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
438 | import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray';
439 | import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
440 |
441 | Tracing.init({
442 | serviceName: 'myservice-opentelemetry-example',
443 | metricReader: new PrometheusExporter({
444 | endpoint: 'metrics',
445 | port: 9464,
446 | }),
447 | instrumentations: [
448 | ...OpenTelemetryModuleDefaultConfig.instrumentations,
449 | new AwsInstrumentation({
450 | suppressInternalInstrumentation: true,
451 | sqsExtractContextPropagationFromPayload: true,
452 | }),
453 | ],
454 | idGenerator: new AWSXRayIdGenerator(),
455 | spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter({})),
456 | textMapPropagator: new AWSXRayPropagator(),
457 | });
458 |
459 | import { NestFactory } from '@nestjs/core';
460 | // ....
461 | ```
462 |
463 | ```ts
464 | // ... app.module.ts
465 | import { Module } from '@nestjs/common';
466 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
467 |
468 | @Module({
469 | imports: [OpenTelemetryModule.forRoot()],
470 | })
471 | export class AppModule {}
472 | ```
473 |
474 | ## Migrating to v5
475 |
476 | In v5, the initialisation method for this library changed to support all the opentelemetry auto-instrumentation libraries like `@opentelemetry/instrumentation-graphql`.
477 | In v4 some of them where not working due to the fact that they were imported after the targeting library, `graphql` lib in the case of `@opentelemetry/instrumentation-graphql`.
478 |
479 | ### v4
480 |
481 | ```ts
482 | import { NestFactory } from '@nestjs/core';
483 | // ....
484 | ```
485 |
486 | ```ts
487 | // app.module.ts
488 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
489 | import { ControllerInjector } from '@amplication/opentelemetry-nestjs';
490 |
491 | @Module({
492 | imports: [
493 | OpenTelemetryModule.forRoot({
494 | serviceName: 'my-service',
495 | spanProcessor: new SimpleSpanProcessor(),
496 | traceAutoInjectors: [ControllerInjector],
497 | }),
498 | ],
499 | })
500 | export class AppModule {}
501 | ```
502 |
503 | ### v5
504 |
505 | ```ts
506 | // main.ts
507 | // at the very top of the file
508 | import { Tracing } from '@amplication/opentelemetry-nestjs';
509 | import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-node';
510 |
511 | Tracing.init({
512 | serviceName: 'my-service',
513 | spanProcessor: new SimpleSpanProcessor(),
514 | });
515 |
516 | import { NestFactory } from '@nestjs/core';
517 | // ....
518 | ```
519 |
520 | ```ts
521 | // app.module.ts
522 | import { OpenTelemetryModule } from '@amplication/opentelemetry-nestjs';
523 | import { ControllerInjector } from '@amplication/opentelemetry-nestjs';
524 |
525 | @Module({
526 | imports: [OpenTelemetryModule.forRoot([ControllerInjector])],
527 | })
528 | export class AppModule {}
529 | ```
530 |
--------------------------------------------------------------------------------
/docs/log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amplication/opentelemetry-nestjs/5ba522e58b97a079caf764ee26a6da4ce1d33f8e/docs/log.png
--------------------------------------------------------------------------------
/docs/trace-flow.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amplication/opentelemetry-nestjs/5ba522e58b97a079caf764ee26a6da4ce1d33f8e/docs/trace-flow.jpeg
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 | module.exports = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'node',
5 | roots: ['/src'],
6 | coverageThreshold: {
7 | global: {
8 | branches: 80,
9 | lines: 90,
10 | },
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@amplication/opentelemetry-nestjs",
3 | "version": "0.0.0-development",
4 | "description": "OpenTelemetry module for Nestjs with auto instrumentation and resource detection. Initially forked from https://github.com/overbit/opentelemetry-nestjs.git",
5 | "author": "Daniele Iasella <2861984+overbit@users.noreply.github.com>",
6 | "license": "MIT",
7 | "main": "dist/index.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/amplication/opentelemetry-nestjs.git"
11 | },
12 | "release": {
13 | "branches": [
14 | "main"
15 | ]
16 | },
17 | "keywords": [
18 | "nestjs",
19 | "opentelemetry",
20 | "tracing",
21 | "observability",
22 | "metric",
23 | "prometheus",
24 | "zipkin",
25 | "jaeger",
26 | "grafana",
27 | "opencensus",
28 | "aws-xray"
29 | ],
30 | "bugs": {
31 | "url": "https://github.com/amplication/opentelemetry-nestjs/issues"
32 | },
33 | "homepage": "https://github.com/amplication/opentelemetry-nestjs#readme",
34 | "scripts": {
35 | "prebuild": "rimraf dist",
36 | "build": "nest build",
37 | "prepublishOnly": "npm run build",
38 | "format": "prettier --write \"**/*.ts\"",
39 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
40 | "test": "jest",
41 | "test:watch": "jest --watch",
42 | "test:cov": "jest --coverage --coverageReporters=text-summary",
43 | "semantic-release": "semantic-release"
44 | },
45 | "dependencies": {},
46 | "devDependencies": {
47 | "@nestjs/cli": "^10.3.2",
48 | "@nestjs/testing": "^10.3.3",
49 | "@types/express": "^4.17.21",
50 | "@types/jest": "^29.5.12",
51 | "@types/node": "^20.11.22",
52 | "@types/supertest": "^6.0.2",
53 | "@typescript-eslint/eslint-plugin": "^7.1.0",
54 | "@typescript-eslint/parser": "^7.1.0",
55 | "eslint": "^8.57.0",
56 | "eslint-config-prettier": "^9.1.0",
57 | "eslint-plugin-prettier": "^5.1.3",
58 | "jest": "^29.7.0",
59 | "prettier": "^3.2.5",
60 | "reflect-metadata": "^0.2.1",
61 | "rimraf": "^5.0.5",
62 | "rxjs": "^7.8.1",
63 | "semantic-release": "^23.0.2",
64 | "supertest": "^6.3.4",
65 | "ts-jest": "^29.1.2",
66 | "ts-loader": "^9.5.1",
67 | "ts-node": "^10.9.2",
68 | "tsconfig-paths": "^4.2.0",
69 | "typescript": "^5.3.3",
70 | "wait-for-expect": "^3.0.2"
71 | },
72 | "peerDependencies": {
73 | "@nestjs/core": " ^8.0.0 || ^9.0.0 || ^10.0.0",
74 | "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
75 | "@nestjs/event-emitter": "^1.0.0 || ^2.0.0",
76 | "@nestjs/microservices": "^8.0.0 || ^9.0.0 || ^10.0.0",
77 | "@nestjs/graphql": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0",
78 | "@nestjs/platform-express": "^8.0.0 || ^9.0.0 || ^10.0.0",
79 | "@nestjs/schedule": "^3.0.0 || ^4.0.1",
80 | "@opentelemetry/auto-instrumentations-node": "^0.41.1",
81 | "@opentelemetry/context-async-hooks": "^1.21.0",
82 | "@opentelemetry/exporter-metrics-otlp-grpc": "^0.48.0",
83 | "@opentelemetry/exporter-metrics-otlp-proto": "^0.48.0",
84 | "@opentelemetry/exporter-trace-otlp-grpc": "^0.48.0",
85 | "@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
86 | "@opentelemetry/exporter-trace-otlp-proto": "^0.48.0",
87 | "@opentelemetry/propagator-b3": "^1.21.0",
88 | "@opentelemetry/propagator-jaeger": "^1.21.0",
89 | "@opentelemetry/resource-detector-container": "^0.3.6",
90 | "@opentelemetry/sdk-node": "^0.48.0",
91 | "@opentelemetry/sdk-trace-base": "^1.21.0"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/Constants.ts:
--------------------------------------------------------------------------------
1 | export enum Constants {
2 | SDK_CONFIG = 'OPEN_TELEMETRY_SDK_CONFIG',
3 | SDK_INJECTORS = 'SDK_INJECTORS',
4 | TRACE_METADATA = 'OPEN_TELEMETRY_TRACE_METADATA',
5 | METRIC_METADATA = 'OPEN_TELEMETRY_METRIC_METADATA',
6 | TRACE_METADATA_ACTIVE = 'OPEN_TELEMETRY_TRACE_METADATA_ACTIVE',
7 | METRIC_METADATA_ACTIVE = 'OPEN_TELEMETRY_METRIC_METADATA_ACTIVE',
8 | TRACER_NAME = '@amplication/opentelemetry-nestjs',
9 | }
10 |
--------------------------------------------------------------------------------
/src/MetaScanner.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-empty-function */
2 | import { MetadataScanner } from './MetaScanner';
3 |
4 | describe('MetadataScanner', () => {
5 | let scanner: MetadataScanner;
6 | beforeEach(() => {
7 | scanner = new MetadataScanner();
8 | });
9 | describe('scanFromPrototype', () => {
10 | class Parent {
11 | constructor() {}
12 | public testParent() {}
13 | public testParent2() {}
14 | get propParent() {
15 | return '';
16 | }
17 | set valParent(value) {}
18 | }
19 |
20 | class Test extends Parent {
21 | constructor() {
22 | super();
23 | }
24 | get prop() {
25 | return '';
26 | }
27 | set val(value) {}
28 | public test() {}
29 | public test2() {}
30 | }
31 |
32 | it('should return only methods', () => {
33 | const methods = scanner.getAllMethodNames(Test.prototype);
34 | expect(methods).toStrictEqual([
35 | 'test',
36 | 'test2',
37 | 'testParent',
38 | 'testParent2',
39 | ]);
40 | });
41 |
42 | it('should return the same instance for the same prototype', () => {
43 | const methods1 = scanner.getAllMethodNames(Test.prototype);
44 | const methods2 = scanner.getAllMethodNames(Test.prototype);
45 | expect(methods1 === methods2).toBeTruthy();
46 | });
47 |
48 | it('should keep compatibility with older methods', () => {
49 | const methods1 = scanner
50 | .getAllMethodNames(Test.prototype)
51 | .map((m) => m[0]);
52 | const methods2 = scanner.scanFromPrototype(
53 | new Test(),
54 | Test.prototype,
55 | (r) => r[0],
56 | );
57 |
58 | expect(methods1).toEqual(methods2);
59 |
60 | const methods3 = scanner.getAllMethodNames(Test.prototype);
61 | const methods4 = [
62 | ...new Set(scanner.getAllFilteredMethodNames(Test.prototype)),
63 | ];
64 |
65 | expect(methods3).toEqual(methods4);
66 | });
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/src/MetaScanner.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common/interfaces';
2 | import {
3 | isConstructor,
4 | isFunction,
5 | isNil,
6 | } from '@nestjs/common/utils/shared.utils';
7 |
8 | export class MetadataScanner {
9 | private readonly cachedScannedPrototypes: Map = new Map();
10 |
11 | /**
12 | * @deprecated
13 | * @see {@link getAllMethodNames}
14 | * @see getAllMethodNames
15 | */
16 | public scanFromPrototype(
17 | instance: T,
18 | prototype: object,
19 | callback: (name: string) => R,
20 | ): R[] {
21 | if (!prototype) {
22 | return [];
23 | }
24 |
25 | const visitedNames = new Map();
26 | const result: R[] = [];
27 |
28 | do {
29 | for (const property of Object.getOwnPropertyNames(prototype)) {
30 | if (visitedNames.has(property)) {
31 | continue;
32 | }
33 |
34 | visitedNames.set(property, true);
35 |
36 | // reason: https://github.com/nestjs/nest/pull/10821#issuecomment-1411916533
37 | const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
38 |
39 | if (
40 | descriptor.set ||
41 | descriptor.get ||
42 | isConstructor(property) ||
43 | !isFunction(prototype[property])
44 | ) {
45 | continue;
46 | }
47 |
48 | const value = callback(property);
49 |
50 | if (isNil(value)) {
51 | continue;
52 | }
53 |
54 | result.push(value);
55 | }
56 | } while (
57 | (prototype = Reflect.getPrototypeOf(prototype)) &&
58 | prototype !== Object.prototype
59 | );
60 |
61 | return result;
62 | }
63 |
64 | /**
65 | * @deprecated
66 | * @see {@link getAllMethodNames}
67 | * @see getAllMethodNames
68 | */
69 | public *getAllFilteredMethodNames(
70 | prototype: object,
71 | ): IterableIterator {
72 | yield* this.getAllMethodNames(prototype);
73 | }
74 |
75 | public getAllMethodNames(prototype: object | null): string[] {
76 | if (!prototype) {
77 | return [];
78 | }
79 |
80 | if (this.cachedScannedPrototypes.has(prototype)) {
81 | return this.cachedScannedPrototypes.get(prototype);
82 | }
83 |
84 | const visitedNames = new Map();
85 | const result: string[] = [];
86 |
87 | this.cachedScannedPrototypes.set(prototype, result);
88 |
89 | do {
90 | for (const property of Object.getOwnPropertyNames(prototype)) {
91 | if (visitedNames.has(property)) {
92 | continue;
93 | }
94 |
95 | visitedNames.set(property, true);
96 |
97 | // reason: https://github.com/nestjs/nest/pull/10821#issuecomment-1411916533
98 | const descriptor = Object.getOwnPropertyDescriptor(prototype, property);
99 |
100 | if (
101 | descriptor.set ||
102 | descriptor.get ||
103 | isConstructor(property) ||
104 | !isFunction(prototype[property])
105 | ) {
106 | continue;
107 | }
108 |
109 | result.push(property);
110 | }
111 | } while (
112 | (prototype = Reflect.getPrototypeOf(prototype)) &&
113 | prototype !== Object.prototype
114 | );
115 |
116 | return result;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/OpenTelemetryModule.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule } from '@nestjs/common';
2 | import { TraceService } from './Trace/TraceService';
3 | import { Constants } from './Constants';
4 | import { OpenTelemetryModuleDefaultConfig } from './OpenTelemetryModuleConfigDefault';
5 | import { FactoryProvider } from '@nestjs/common/interfaces/modules/provider.interface';
6 | import { OpenTelemetryModuleAsyncOption } from './OpenTelemetryModuleAsyncOption';
7 | import { DecoratorInjector } from './Trace/Injectors/DecoratorInjector';
8 | import { ModuleRef } from '@nestjs/core';
9 | import { EventEmitterModule } from '@nestjs/event-emitter';
10 | import { Tracer } from '@opentelemetry/sdk-trace-node';
11 | import { OpenTelemetryModuleConfig } from './OpenTelemetryModuleConfig.interface';
12 |
13 | export class OpenTelemetryModule {
14 | static forRoot(
15 | traceAutoInjectors?: OpenTelemetryModuleConfig,
16 | ): DynamicModule {
17 | const injectors = traceAutoInjectors ?? OpenTelemetryModuleDefaultConfig;
18 |
19 | return {
20 | global: true,
21 | module: OpenTelemetryModule,
22 | imports: [EventEmitterModule.forRoot()],
23 | providers: [
24 | ...injectors,
25 | TraceService,
26 | DecoratorInjector,
27 | this.buildInjectors(injectors),
28 | this.buildTracer(),
29 | ],
30 | exports: [TraceService, Tracer],
31 | };
32 | }
33 |
34 | private static buildInjectors(
35 | injectors: OpenTelemetryModuleConfig = [],
36 | ): FactoryProvider {
37 | return {
38 | provide: Constants.SDK_INJECTORS,
39 | useFactory: async (...injectors) => {
40 | for await (const injector of injectors) {
41 | if (injector['inject']) await injector.inject();
42 | }
43 | },
44 | inject: [
45 | DecoratorInjector,
46 | // eslint-disable-next-line @typescript-eslint/ban-types
47 | ...(injectors as Function[]),
48 | ],
49 | };
50 | }
51 |
52 | static async forRootAsync(
53 | configuration: OpenTelemetryModuleAsyncOption = {},
54 | ): Promise {
55 | return {
56 | global: true,
57 | module: OpenTelemetryModule,
58 | // eslint-disable-next-line no-unsafe-optional-chaining
59 | imports: [...configuration?.imports, EventEmitterModule.forRoot()],
60 | providers: [
61 | TraceService,
62 | this.buildAsyncInjectors(),
63 | this.buildTracer(),
64 | {
65 | provide: Constants.SDK_CONFIG,
66 | useFactory: configuration.useFactory,
67 | inject: configuration.inject,
68 | },
69 | ],
70 | exports: [TraceService, Tracer],
71 | };
72 | }
73 |
74 | private static buildAsyncInjectors(): FactoryProvider {
75 | return {
76 | provide: Constants.SDK_INJECTORS,
77 | useFactory: async (traceAutoInjectors, moduleRef: ModuleRef) => {
78 | const injectors =
79 | traceAutoInjectors ?? OpenTelemetryModuleDefaultConfig;
80 |
81 | const decoratorInjector = await moduleRef.create(DecoratorInjector);
82 | await decoratorInjector.inject();
83 |
84 | for await (const injector of injectors) {
85 | const created = await moduleRef.create(injector);
86 | if (created['inject']) await created.inject();
87 | }
88 |
89 | return {};
90 | },
91 | inject: [Constants.SDK_CONFIG, ModuleRef],
92 | };
93 | }
94 |
95 | private static buildTracer() {
96 | return {
97 | provide: Tracer,
98 | useFactory: (traceService: TraceService) => traceService.getTracer(),
99 | inject: [TraceService],
100 | };
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/OpenTelemetryModuleAsyncOption.ts:
--------------------------------------------------------------------------------
1 | import { ModuleMetadata } from '@nestjs/common';
2 | import type { OpenTelemetryModuleConfig } from './OpenTelemetryModuleConfig.interface';
3 |
4 | export interface OpenTelemetryModuleAsyncOption
5 | extends Pick {
6 | useFactory?: (
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | ...args: any[]
9 | ) =>
10 | | Promise>
11 | | Partial;
12 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
13 | inject?: any[];
14 | }
15 |
--------------------------------------------------------------------------------
/src/OpenTelemetryModuleConfig.interface.ts:
--------------------------------------------------------------------------------
1 | import type { Provider } from '@nestjs/common/interfaces/modules/provider.interface';
2 | import type { Injector } from './Trace/Injectors/Injector';
3 |
4 | export type OpenTelemetryModuleConfig = Provider[];
5 |
--------------------------------------------------------------------------------
/src/OpenTelemetryModuleConfigDefault.ts:
--------------------------------------------------------------------------------
1 | import { ControllerInjector } from './Trace/Injectors/ControllerInjector';
2 | import { GuardInjector } from './Trace/Injectors/GuardInjector';
3 | import { EventEmitterInjector } from './Trace/Injectors/EventEmitterInjector';
4 | import { ScheduleInjector } from './Trace/Injectors/ScheduleInjector';
5 | import { PipeInjector } from './Trace/Injectors/PipeInjector';
6 | import { ConsoleLoggerInjector } from './Trace/Injectors/ConsoleLoggerInjector';
7 | import { OpenTelemetryModuleConfig } from './OpenTelemetryModuleConfig.interface';
8 | import { GraphQLResolverInjector } from './Trace/Injectors/GraphQLResolverInjector';
9 |
10 | export const OpenTelemetryModuleDefaultConfig = [
11 | ControllerInjector,
12 | GraphQLResolverInjector,
13 | GuardInjector,
14 | EventEmitterInjector,
15 | ScheduleInjector,
16 | PipeInjector,
17 | ConsoleLoggerInjector,
18 | ];
19 |
--------------------------------------------------------------------------------
/src/Trace/Decorators/Span.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 | import { Constants } from '../../Constants';
3 |
4 | /**
5 | * A decorator to mark a method as a span
6 | * @param name The name of the span
7 | */
8 | export const Span = (name?: string) =>
9 | SetMetadata(Constants.TRACE_METADATA, name);
10 |
--------------------------------------------------------------------------------
/src/Trace/Decorators/Traceable.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 | import { Constants } from '../../Constants';
3 |
4 | /**
5 | * Decorator to mark all methods of a class as a traceable
6 | */
7 | export const Traceable = (name?: string) =>
8 | SetMetadata(Constants.TRACE_METADATA, name);
9 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/BaseTraceInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { Tracing } from '../../Tracing';
3 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
4 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
5 | import { Controller, Get, Injectable } from '@nestjs/common';
6 | import { Span } from '../Decorators/Span';
7 | import * as request from 'supertest';
8 | import { ControllerInjector } from './ControllerInjector';
9 |
10 | describe('Base Trace Injector Test', () => {
11 | const sdkModule = OpenTelemetryModule.forRoot([ControllerInjector]);
12 | let exporterSpy: jest.SpyInstance;
13 |
14 | beforeEach(() => {
15 | const exporter = new NoopSpanProcessor();
16 | exporterSpy = jest.spyOn(exporter, 'onStart');
17 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
18 | });
19 |
20 | afterEach(() => {
21 | exporterSpy.mockClear();
22 | exporterSpy.mockReset();
23 | });
24 |
25 | it('should create spans that inherit the ids of their parents', async () => {
26 | // given
27 | @Injectable()
28 | class HelloService {
29 | @Span()
30 | hello() {
31 | this.helloAgain();
32 | }
33 | @Span()
34 | helloAgain() {} // eslint-disable-line @typescript-eslint/no-empty-function
35 | }
36 |
37 | @Controller('hello')
38 | class HelloController {
39 | constructor(private service: HelloService) {}
40 | @Get()
41 | hi() {
42 | return this.service.hello();
43 | }
44 | }
45 |
46 | const context = await Test.createTestingModule({
47 | imports: [sdkModule],
48 | providers: [HelloService],
49 | controllers: [HelloController],
50 | }).compile();
51 | const app = context.createNestApplication();
52 | await app.init();
53 |
54 | //when
55 | await request(app.getHttpServer()).get('/hello').send().expect(200);
56 |
57 | //then
58 | const [[parent], [childOfParent], [childOfChild]] = exporterSpy.mock.calls;
59 | expect(parent.parentSpanId).toBeUndefined();
60 | expect(childOfParent.parentSpanId).toBe(parent.spanContext().spanId);
61 | expect(childOfChild.parentSpanId).toBe(childOfParent.spanContext().spanId);
62 |
63 | await app.close();
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/BaseTraceInjector.ts:
--------------------------------------------------------------------------------
1 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
2 | import { Constants } from '../../Constants';
3 | import { ModulesContainer } from '@nestjs/core';
4 | import { Controller, Injectable } from '@nestjs/common/interfaces';
5 | import { PATH_METADATA } from '@nestjs/common/constants';
6 | import { PATTERN_METADATA } from '@nestjs/microservices/constants';
7 | import { TraceWrapper } from '../TraceWrapper';
8 | import { SpanKind } from '@opentelemetry/api';
9 | import { MetadataScanner } from '../../MetaScanner';
10 |
11 | export class BaseTraceInjector {
12 | protected readonly metadataScanner: MetadataScanner = new MetadataScanner();
13 |
14 | constructor(protected readonly modulesContainer: ModulesContainer) {}
15 |
16 | protected *getControllers(): Generator> {
17 | for (const module of this.modulesContainer.values()) {
18 | for (const controller of module.controllers.values()) {
19 | if (controller && controller.metatype?.prototype) {
20 | yield controller as InstanceWrapper;
21 | }
22 | }
23 | }
24 | }
25 |
26 | protected *getProviders(): Generator> {
27 | for (const module of this.modulesContainer.values()) {
28 | for (const provider of module.providers.values()) {
29 | if (provider && provider.metatype?.prototype) {
30 | yield provider as InstanceWrapper;
31 | }
32 | }
33 | }
34 | }
35 |
36 | protected isPath(prototype): boolean {
37 | return Reflect.hasMetadata(PATH_METADATA, prototype);
38 | }
39 |
40 | protected isMicroservice(prototype): boolean {
41 | return Reflect.hasMetadata(PATTERN_METADATA, prototype);
42 | }
43 |
44 | protected isAffected(prototype): boolean {
45 | return Reflect.hasMetadata(Constants.TRACE_METADATA_ACTIVE, prototype);
46 | }
47 |
48 | protected getTraceName(prototype): string {
49 | return Reflect.getMetadata(Constants.TRACE_METADATA, prototype);
50 | }
51 |
52 | protected isDecorated(prototype): boolean {
53 | return Reflect.hasMetadata(Constants.TRACE_METADATA, prototype);
54 | }
55 |
56 | protected reDecorate(source, destination) {
57 | const keys = Reflect.getMetadataKeys(source);
58 |
59 | for (const key of keys) {
60 | const meta = Reflect.getMetadata(key, source);
61 | Reflect.defineMetadata(key, meta, destination);
62 | }
63 | }
64 |
65 | protected wrap(
66 | prototype: Record,
67 | traceName,
68 | attributes = {},
69 | spanKind?: SpanKind,
70 | ) {
71 | return TraceWrapper.wrap(prototype, traceName, attributes, spanKind);
72 | }
73 |
74 | protected affect(prototype) {
75 | Reflect.defineMetadata(Constants.TRACE_METADATA_ACTIVE, 1, prototype);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/ConsoleLoggerInjector.ts:
--------------------------------------------------------------------------------
1 | import { ConsoleLogger, Injectable } from '@nestjs/common';
2 | import { Injector } from './Injector';
3 | import { context, trace } from '@opentelemetry/api';
4 |
5 | @Injectable()
6 | export class ConsoleLoggerInjector implements Injector {
7 | public inject() {
8 | ConsoleLogger.prototype.log = this.wrapPrototype(
9 | ConsoleLogger.prototype.log,
10 | );
11 | ConsoleLogger.prototype.debug = this.wrapPrototype(
12 | ConsoleLogger.prototype.debug,
13 | );
14 | ConsoleLogger.prototype.error = this.wrapPrototype(
15 | ConsoleLogger.prototype.error,
16 | );
17 | ConsoleLogger.prototype.verbose = this.wrapPrototype(
18 | ConsoleLogger.prototype.verbose,
19 | );
20 | ConsoleLogger.prototype.warn = this.wrapPrototype(
21 | ConsoleLogger.prototype.warn,
22 | );
23 | }
24 |
25 | private wrapPrototype(prototype) {
26 | return {
27 | [prototype.name]: function (...args: any[]) {
28 | args[0] = ConsoleLoggerInjector.getMessage(args[0]);
29 | prototype.apply(this, args);
30 | },
31 | }[prototype.name];
32 | }
33 |
34 | private static getMessage(message: string) {
35 | const currentSpan = trace.getSpan(context.active());
36 | if (!currentSpan) return message;
37 |
38 | const spanContext = trace.getSpan(context.active()).spanContext();
39 | currentSpan.addEvent(message);
40 |
41 | return `[${spanContext.traceId}] ${message}`;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/ControllerInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { Tracing } from '../../Tracing';
3 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
4 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
5 | import { Controller, ForbiddenException, Get } from '@nestjs/common';
6 | import { Span } from '../Decorators/Span';
7 | import * as request from 'supertest';
8 | import { ControllerInjector } from './ControllerInjector';
9 | import waitForExpect from 'wait-for-expect';
10 | import { EventPattern, MessagePattern } from '@nestjs/microservices';
11 | import { SpanKind, SpanStatusCode } from '@opentelemetry/api';
12 |
13 | describe('Tracing Controller Injector Test', () => {
14 | const sdkModule = OpenTelemetryModule.forRoot([ControllerInjector]);
15 | let exporterSpy: jest.SpyInstance;
16 | const exporter = new NoopSpanProcessor();
17 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
18 |
19 | beforeEach(() => {
20 | exporterSpy = jest.spyOn(exporter, 'onStart');
21 | });
22 |
23 | afterEach(() => {
24 | exporterSpy.mockClear();
25 | exporterSpy.mockReset();
26 | });
27 |
28 | describe('for microservices', () => {
29 | it(`should trace controller method using MessagePattern`, async () => {
30 | // given
31 | @Controller('hello')
32 | class HelloController {
33 | @MessagePattern('time.us.*')
34 | // eslint-disable-next-line @typescript-eslint/no-empty-function
35 | message() {}
36 | }
37 |
38 | const context = await Test.createTestingModule({
39 | imports: [sdkModule],
40 | controllers: [HelloController],
41 | }).compile();
42 | const app = context.createNestApplication();
43 | await app.init();
44 |
45 | // when
46 | await app.get(HelloController).message();
47 |
48 | //then
49 | await waitForExpect(() =>
50 | expect(exporterSpy).toHaveBeenCalledWith(
51 | expect.objectContaining({
52 | name: 'Controller->HelloController.message',
53 | kind: SpanKind.SERVER,
54 | }),
55 | expect.any(Object),
56 | ),
57 | );
58 |
59 | await app.close();
60 | });
61 |
62 | it(`should trace controller method using EventPattern`, async () => {
63 | // given
64 | @Controller('hello')
65 | class HelloController {
66 | @EventPattern('user.created')
67 | // eslint-disable-next-line @typescript-eslint/no-empty-function
68 | event() {}
69 | }
70 |
71 | const context = await Test.createTestingModule({
72 | imports: [sdkModule],
73 | controllers: [HelloController],
74 | }).compile();
75 | const app = context.createNestApplication();
76 | await app.init();
77 |
78 | // when
79 | await app.get(HelloController).event();
80 |
81 | //then
82 | await waitForExpect(() =>
83 | expect(exporterSpy).toHaveBeenCalledWith(
84 | expect.objectContaining({
85 | name: 'Controller->HelloController.event',
86 | kind: SpanKind.SERVER,
87 | }),
88 | expect.any(Object),
89 | ),
90 | );
91 |
92 | await app.close();
93 | });
94 |
95 | it(`should not trace controller method if it is not a microservice client`, async () => {
96 | // given
97 | @Controller('hello')
98 | class HelloController {
99 | @MessagePattern('time.us.*')
100 | // eslint-disable-next-line @typescript-eslint/no-empty-function
101 | message() {}
102 | @EventPattern('time.us.*')
103 | // eslint-disable-next-line @typescript-eslint/no-empty-function
104 | event() {}
105 | // eslint-disable-next-line @typescript-eslint/no-empty-function
106 | other() {}
107 | }
108 |
109 | const context = await Test.createTestingModule({
110 | imports: [sdkModule],
111 | controllers: [HelloController],
112 | }).compile();
113 | const app = context.createNestApplication();
114 | await app.init();
115 | const helloController = app.get(HelloController);
116 |
117 | //when
118 | helloController.message();
119 | helloController.event();
120 | helloController.other();
121 |
122 | //then
123 | await waitForExpect(() =>
124 | expect(exporterSpy).not.toHaveBeenCalledWith(
125 | expect.objectContaining({
126 | name: 'Controller->HelloController.other',
127 | }),
128 | expect.any(Object),
129 | ),
130 | );
131 |
132 | await app.close();
133 | });
134 |
135 | it(`should trace controller method exception`, async () => {
136 | // given @Controller('hello')
137 | @Controller()
138 | class HelloController {
139 | @EventPattern('user.created')
140 | // eslint-disable-next-line @typescript-eslint/no-empty-function
141 | event() {
142 | throw new Error("I'm an error");
143 | }
144 | }
145 |
146 | const context = await Test.createTestingModule({
147 | imports: [sdkModule],
148 | controllers: [HelloController],
149 | }).compile();
150 | const app = context.createNestApplication();
151 | await app.init();
152 |
153 | // when
154 | try {
155 | await app.get(HelloController).event();
156 | } catch (error) {}
157 |
158 | //then
159 | await waitForExpect(() =>
160 | expect(exporterSpy).toHaveBeenCalledWith(
161 | expect.objectContaining({
162 | name: 'Controller->HelloController.event',
163 | status: {
164 | code: SpanStatusCode.ERROR,
165 | message: "I'm an error",
166 | },
167 | }),
168 | expect.any(Object),
169 | ),
170 | );
171 |
172 | await app.close();
173 | });
174 | });
175 |
176 | describe('for http', () => {
177 | it(`should trace controller method`, async () => {
178 | // given
179 | @Controller('hello')
180 | class HelloController {
181 | @Get()
182 | // eslint-disable-next-line @typescript-eslint/no-empty-function
183 | hi() {}
184 | }
185 |
186 | const context = await Test.createTestingModule({
187 | imports: [sdkModule],
188 | controllers: [HelloController],
189 | }).compile();
190 | const app = context.createNestApplication();
191 | await app.init();
192 |
193 | // when
194 | await request(app.getHttpServer()).get('/hello').send().expect(200);
195 |
196 | //then
197 | await waitForExpect(() =>
198 | expect(exporterSpy).toHaveBeenCalledWith(
199 | expect.objectContaining({
200 | name: 'Controller->HelloController.hi',
201 | kind: SpanKind.SERVER,
202 | }),
203 | expect.any(Object),
204 | ),
205 | );
206 |
207 | await app.close();
208 | });
209 |
210 | it(`should trace controller method exception`, async () => {
211 | // given
212 | @Controller('hello')
213 | class HelloController {
214 | @Get()
215 | hi() {
216 | throw new ForbiddenException();
217 | }
218 | }
219 |
220 | const context = await Test.createTestingModule({
221 | imports: [sdkModule],
222 | controllers: [HelloController],
223 | }).compile();
224 | const app = context.createNestApplication();
225 | await app.init();
226 |
227 | // when
228 | await request(app.getHttpServer()).get('/hello').send().expect(403);
229 |
230 | //then
231 | await waitForExpect(() =>
232 | expect(exporterSpy).toHaveBeenCalledWith(
233 | expect.objectContaining({
234 | name: 'Controller->HelloController.hi',
235 | status: {
236 | code: SpanStatusCode.ERROR,
237 | message: 'Forbidden',
238 | },
239 | }),
240 | expect.any(Object),
241 | ),
242 | );
243 |
244 | await app.close();
245 | });
246 |
247 | it(`should not trace controller method if there is no path`, async () => {
248 | // given
249 | @Controller('hello')
250 | class HelloController {
251 | // eslint-disable-next-line @typescript-eslint/no-empty-function
252 | hi() {}
253 | }
254 |
255 | const context = await Test.createTestingModule({
256 | imports: [sdkModule],
257 | controllers: [HelloController],
258 | }).compile();
259 | const app = context.createNestApplication();
260 | await app.init();
261 | const helloController = app.get(HelloController);
262 |
263 | //when
264 | helloController.hi();
265 |
266 | //then
267 | await waitForExpect(() =>
268 | expect(exporterSpy).not.toHaveBeenCalledWith(
269 | expect.objectContaining({ name: 'Controller->HelloController.hi' }),
270 | expect.any(Object),
271 | ),
272 | );
273 |
274 | await app.close();
275 | });
276 |
277 | it(`should not trace controller method if already decorated`, async () => {
278 | // given
279 | @Controller('hello')
280 | class HelloController {
281 | @Get()
282 | @Span('SLM_CNM')
283 | // eslint-disable-next-line @typescript-eslint/no-empty-function
284 | hi() {}
285 | }
286 |
287 | const context = await Test.createTestingModule({
288 | imports: [sdkModule],
289 | controllers: [HelloController],
290 | }).compile();
291 | const app = context.createNestApplication();
292 | await app.init();
293 |
294 | // when
295 | await request(app.getHttpServer()).get('/hello').send().expect(200);
296 |
297 | // then
298 | await waitForExpect(() =>
299 | expect(exporterSpy).toHaveBeenCalledWith(
300 | expect.objectContaining({
301 | name: 'Controller->HelloController.SLM_CNM',
302 | }),
303 | expect.any(Object),
304 | ),
305 | );
306 |
307 | await app.close();
308 | });
309 | });
310 | });
311 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/ControllerInjector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { ModulesContainer } from '@nestjs/core';
3 | import { BaseTraceInjector } from './BaseTraceInjector';
4 | import { Injector } from './Injector';
5 | import { SpanKind } from '@opentelemetry/api';
6 |
7 | @Injectable()
8 | export class ControllerInjector extends BaseTraceInjector implements Injector {
9 | private readonly loggerService = new Logger();
10 |
11 | constructor(protected readonly modulesContainer: ModulesContainer) {
12 | super(modulesContainer);
13 | }
14 |
15 | public inject() {
16 | const controllers = this.getControllers();
17 |
18 | for (const controller of controllers) {
19 | const keys = this.metadataScanner.getAllMethodNames(
20 | controller.metatype.prototype,
21 | );
22 |
23 | for (const key of keys) {
24 | if (
25 | !this.isDecorated(controller.metatype.prototype[key]) &&
26 | !this.isAffected(controller.metatype.prototype[key]) &&
27 | (this.isPath(controller.metatype.prototype[key]) ||
28 | this.isMicroservice(controller.metatype.prototype[key]))
29 | ) {
30 | const traceName = `Controller->${controller.name}.${controller.metatype.prototype[key].name}`;
31 | const method = this.wrap(
32 | controller.metatype.prototype[key],
33 | traceName,
34 | {
35 | controller: controller.name,
36 | method: controller.metatype.prototype[key].name,
37 | },
38 | SpanKind.SERVER,
39 | );
40 | this.reDecorate(controller.metatype.prototype[key], method);
41 |
42 | controller.metatype.prototype[key] = method;
43 | this.loggerService.log(
44 | `Mapped ${controller.name}.${key}`,
45 | this.constructor.name,
46 | );
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/DecoratorInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import { Controller, Get, Injectable } from '@nestjs/common';
5 | import { Span } from '../Decorators/Span';
6 | import * as request from 'supertest';
7 | import { Constants } from '../../Constants';
8 | import { Tracing } from '../../Tracing';
9 |
10 | describe('Tracing Decorator Injector Test', () => {
11 | const sdkModule = OpenTelemetryModule.forRoot();
12 | let exporterSpy: jest.SpyInstance;
13 | const exporter = new NoopSpanProcessor();
14 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
15 |
16 | beforeEach(() => {
17 | exporterSpy = jest.spyOn(exporter, 'onStart');
18 | });
19 |
20 | afterEach(() => {
21 | exporterSpy.mockClear();
22 | exporterSpy.mockReset();
23 | });
24 |
25 | it(`should trace decorated provider method`, async () => {
26 | // given
27 | @Injectable()
28 | class HelloService {
29 | @Span()
30 | // eslint-disable-next-line @typescript-eslint/no-empty-function
31 | hi() {}
32 | }
33 | const context = await Test.createTestingModule({
34 | imports: [sdkModule],
35 | providers: [HelloService],
36 | }).compile();
37 | const app = context.createNestApplication();
38 | const helloService = app.get(HelloService);
39 |
40 | // when
41 | helloService.hi();
42 |
43 | //then
44 | expect(exporterSpy).toHaveBeenCalledWith(
45 | expect.objectContaining({ name: 'Provider->HelloService.hi' }),
46 | expect.any(Object),
47 | );
48 |
49 | await app.close();
50 | });
51 |
52 | it(`should trace decorated controller method`, async () => {
53 | // given
54 | @Controller('hello')
55 | class HelloController {
56 | @Span()
57 | @Get()
58 | // eslint-disable-next-line @typescript-eslint/no-empty-function
59 | hi() {}
60 | }
61 | const context = await Test.createTestingModule({
62 | imports: [sdkModule],
63 | controllers: [HelloController],
64 | }).compile();
65 | const app = context.createNestApplication();
66 | await app.init();
67 |
68 | // when
69 | await request(app.getHttpServer()).get('/hello').send().expect(200);
70 |
71 | //then
72 | expect(exporterSpy).toHaveBeenCalledWith(
73 | expect.objectContaining({ name: 'Controller->HelloController.hi' }),
74 | expect.any(Object),
75 | );
76 |
77 | await app.close();
78 | });
79 |
80 | it(`should trace decorated controller method with custom trace name`, async () => {
81 | // given
82 | @Controller('hello')
83 | class HelloController {
84 | @Span('MAVI_VATAN')
85 | @Get()
86 | // eslint-disable-next-line @typescript-eslint/no-empty-function
87 | hi() {}
88 | }
89 | const context = await Test.createTestingModule({
90 | imports: [sdkModule],
91 | controllers: [HelloController],
92 | }).compile();
93 | const app = context.createNestApplication();
94 | await app.init();
95 |
96 | // when
97 | await request(app.getHttpServer()).get('/hello').send().expect(200);
98 |
99 | //then
100 | expect(exporterSpy).toHaveBeenCalledWith(
101 | expect.objectContaining({
102 | name: 'Controller->HelloController.MAVI_VATAN',
103 | }),
104 | expect.any(Object),
105 | );
106 |
107 | await app.close();
108 | });
109 |
110 | it(`should not trace already tracing prototype`, async () => {
111 | // given
112 | @Injectable()
113 | class HelloService {
114 | @Span()
115 | // eslint-disable-next-line @typescript-eslint/no-empty-function
116 | hi() {}
117 | }
118 | Reflect.defineMetadata(
119 | Constants.TRACE_METADATA_ACTIVE,
120 | 1,
121 | HelloService.prototype.hi,
122 | );
123 |
124 | const context = await Test.createTestingModule({
125 | imports: [sdkModule],
126 | providers: [HelloService],
127 | }).compile();
128 | const app = context.createNestApplication();
129 | const helloService = app.get(HelloService);
130 |
131 | // when
132 | helloService.hi();
133 |
134 | //then
135 | expect(exporterSpy).not.toHaveBeenCalledWith(
136 | expect.objectContaining({ name: 'Provider->HelloService.hi' }),
137 | expect.any(Object),
138 | );
139 |
140 | await app.close();
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/DecoratorInjector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { ModulesContainer } from '@nestjs/core';
3 | import { Injector } from './Injector';
4 | import { BaseTraceInjector } from './BaseTraceInjector';
5 |
6 | @Injectable()
7 | export class DecoratorInjector extends BaseTraceInjector implements Injector {
8 | private readonly loggerService = new Logger();
9 |
10 | constructor(protected readonly modulesContainer: ModulesContainer) {
11 | super(modulesContainer);
12 | }
13 |
14 | public inject() {
15 | this.injectProviders();
16 | this.injectControllers();
17 | }
18 |
19 | private injectProviders() {
20 | const providers = this.getProviders();
21 |
22 | for (const provider of providers) {
23 | const keys = this.metadataScanner.getAllMethodNames(
24 | provider.metatype.prototype,
25 | );
26 |
27 | for (const key of keys) {
28 | if (
29 | (this.isDecorated(provider.metatype) ||
30 | this.isDecorated(provider.metatype.prototype[key])) &&
31 | !this.isAffected(provider.metatype.prototype[key])
32 | ) {
33 | provider.metatype.prototype[key] = this.wrap(
34 | provider.metatype.prototype[key],
35 | this.getPrefix(
36 | provider.metatype.prototype[key],
37 | `Provider->${provider.name}`,
38 | ),
39 | );
40 | this.loggerService.log(
41 | `Mapped ${provider.name}.${key}`,
42 | this.constructor.name,
43 | );
44 | }
45 | }
46 | }
47 | }
48 |
49 | private injectControllers() {
50 | const controllers = this.getControllers();
51 |
52 | for (const controller of controllers) {
53 | const isControllerDecorated = this.isDecorated(controller.metatype);
54 |
55 | const keys = this.metadataScanner.getAllMethodNames(
56 | controller.metatype.prototype,
57 | );
58 |
59 | for (const key of keys) {
60 | if (
61 | (isControllerDecorated &&
62 | !this.isAffected(controller.metatype.prototype[key])) ||
63 | (this.isDecorated(controller.metatype.prototype[key]) &&
64 | !this.isAffected(controller.metatype.prototype[key]))
65 | ) {
66 | const method = this.wrap(
67 | controller.metatype.prototype[key],
68 | this.getPrefix(
69 | controller.metatype.prototype[key],
70 | `Controller->${controller.name}`,
71 | ),
72 | );
73 | this.reDecorate(controller.metatype.prototype[key], method);
74 |
75 | controller.metatype.prototype[key] = method;
76 | this.loggerService.log(
77 | `Mapped ${controller.name}.${key}`,
78 | this.constructor.name,
79 | );
80 | }
81 | }
82 | }
83 | }
84 |
85 | private getPrefix(prototype, type: string) {
86 | const name = this.getTraceName(prototype);
87 | if (name) {
88 | return `${type}.${name}`;
89 | }
90 | return `${type}.${prototype.name}`;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/EventEmitterInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import { Injectable } from '@nestjs/common';
5 | import { Span } from '../Decorators/Span';
6 | import { EventEmitterInjector } from './EventEmitterInjector';
7 | import { OnEvent } from '@nestjs/event-emitter';
8 | import { Tracing } from '../../Tracing';
9 |
10 | describe('Tracing Event Emitter Injector Test', () => {
11 | const sdkModule = OpenTelemetryModule.forRoot([EventEmitterInjector]);
12 | let exporterSpy: jest.SpyInstance;
13 | const exporter = new NoopSpanProcessor();
14 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
15 |
16 | beforeEach(() => {
17 | exporterSpy = jest.spyOn(exporter, 'onStart');
18 | });
19 |
20 | afterEach(() => {
21 | exporterSpy.mockClear();
22 | exporterSpy.mockReset();
23 | });
24 |
25 | it(`should trace event consumer method`, async () => {
26 | // given
27 | @Injectable()
28 | class HelloService {
29 | @OnEvent('selam')
30 | // eslint-disable-next-line @typescript-eslint/no-empty-function
31 | hi() {}
32 | }
33 | const context = await Test.createTestingModule({
34 | imports: [sdkModule],
35 | providers: [HelloService],
36 | }).compile();
37 | const app = context.createNestApplication();
38 | const helloService = app.get(HelloService);
39 | await app.init();
40 |
41 | // when
42 | helloService.hi();
43 |
44 | //then
45 | expect(exporterSpy).toHaveBeenCalledWith(
46 | expect.objectContaining({ name: 'Event->HelloService.selam' }),
47 | expect.any(Object),
48 | );
49 |
50 | await app.close();
51 | });
52 |
53 | it(`should not trace already decorated event consumer method`, async () => {
54 | // given
55 | @Injectable()
56 | class HelloService {
57 | @Span('untraceable')
58 | @OnEvent('tb2')
59 | // eslint-disable-next-line @typescript-eslint/no-empty-function
60 | hi() {}
61 | }
62 | const context = await Test.createTestingModule({
63 | imports: [sdkModule],
64 | providers: [HelloService],
65 | }).compile();
66 | const app = context.createNestApplication();
67 | const helloService = app.get(HelloService);
68 | await app.init();
69 |
70 | // when
71 | helloService.hi();
72 |
73 | //then
74 | expect(exporterSpy).toHaveBeenCalledWith(
75 | expect.objectContaining({ name: 'Provider->HelloService.untraceable' }),
76 | expect.any(Object),
77 | );
78 |
79 | await app.close();
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/EventEmitterInjector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { Injector } from './Injector';
3 | import { ModulesContainer } from '@nestjs/core';
4 | import { BaseTraceInjector } from './BaseTraceInjector';
5 |
6 | @Injectable()
7 | export class EventEmitterInjector
8 | extends BaseTraceInjector
9 | implements Injector
10 | {
11 | private static EVENT_LISTENER_METADATA = 'EVENT_LISTENER_METADATA';
12 |
13 | private readonly loggerService = new Logger();
14 |
15 | constructor(protected readonly modulesContainer: ModulesContainer) {
16 | super(modulesContainer);
17 | }
18 |
19 | public inject() {
20 | const providers = this.getProviders();
21 |
22 | for (const provider of providers) {
23 | const keys = this.metadataScanner.getAllMethodNames(
24 | provider.metatype.prototype,
25 | );
26 |
27 | for (const key of keys) {
28 | if (
29 | !this.isDecorated(provider.metatype.prototype[key]) &&
30 | !this.isAffected(provider.metatype.prototype[key]) &&
31 | this.isEventConsumer(provider.metatype.prototype[key])
32 | ) {
33 | const eventName = this.getEventName(provider.metatype.prototype[key]);
34 | provider.metatype.prototype[key] = this.wrap(
35 | provider.metatype.prototype[key],
36 | `Event->${provider.name}.${eventName}`,
37 | {
38 | instance: provider.name,
39 | method: provider.metatype.prototype[key].name,
40 | event: eventName,
41 | },
42 | );
43 | this.loggerService.log(
44 | `Mapped ${provider.name}.${key}`,
45 | this.constructor.name,
46 | );
47 | }
48 | }
49 | }
50 | }
51 |
52 | private isEventConsumer(prototype): boolean {
53 | return Reflect.getMetadata(
54 | EventEmitterInjector.EVENT_LISTENER_METADATA,
55 | prototype,
56 | );
57 | }
58 |
59 | private getEventName(prototype): string {
60 | const metadata: Array<{ event: string }> = Reflect.getMetadata(
61 | EventEmitterInjector.EVENT_LISTENER_METADATA,
62 | prototype,
63 | );
64 | return metadata[0].event;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/GraphQLResolverInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import { Injectable } from '@nestjs/common';
5 | import { Constants } from '../../Constants';
6 | import { Resolver, Query, Subscription, Mutation } from '@nestjs/graphql';
7 | import { Tracing } from '../../Tracing';
8 |
9 | describe('Tracing Decorator Injector Test', () => {
10 | const sdkModule = OpenTelemetryModule.forRoot();
11 | let exporterSpy: jest.SpyInstance;
12 | const exporter = new NoopSpanProcessor();
13 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
14 |
15 | beforeEach(() => {
16 | exporterSpy = jest.spyOn(exporter, 'onStart');
17 | });
18 |
19 | afterEach(() => {
20 | exporterSpy.mockClear();
21 | exporterSpy.mockReset();
22 | });
23 |
24 | it(`should trace graphql resolver provider Query method`, async () => {
25 | // given
26 | @Resolver(() => {
27 | /***/
28 | })
29 | class HelloService {
30 | @Query(() => [String], {
31 | nullable: false,
32 | })
33 | // eslint-disable-next-line @typescript-eslint/no-empty-function
34 | hi() {}
35 | }
36 | const context = await Test.createTestingModule({
37 | imports: [sdkModule],
38 | providers: [HelloService],
39 | }).compile();
40 | const app = context.createNestApplication();
41 | const helloService = app.get(HelloService);
42 |
43 | // when
44 | helloService.hi();
45 |
46 | //then
47 | expect(exporterSpy).toHaveBeenCalledWith(
48 | expect.objectContaining({ name: 'Resolver->HelloService.hi' }),
49 | expect.any(Object),
50 | );
51 |
52 | await app.close();
53 | });
54 |
55 | it(`should trace graphql resolver provider Mutation method`, async () => {
56 | // given
57 | @Resolver(() => {
58 | /***/
59 | })
60 | class HelloService {
61 | @Mutation(() => [String], {
62 | nullable: false,
63 | })
64 | // eslint-disable-next-line @typescript-eslint/no-empty-function
65 | hi() {}
66 | }
67 | const context = await Test.createTestingModule({
68 | imports: [sdkModule],
69 | providers: [HelloService],
70 | }).compile();
71 | const app = context.createNestApplication();
72 | const helloService = app.get(HelloService);
73 |
74 | // when
75 | helloService.hi();
76 |
77 | //then
78 | expect(exporterSpy).toHaveBeenCalledWith(
79 | expect.objectContaining({ name: 'Resolver->HelloService.hi' }),
80 | expect.any(Object),
81 | );
82 |
83 | await app.close();
84 | });
85 | it(`should trace graphql resolver provider Subscription method`, async () => {
86 | // given
87 | @Resolver(() => {
88 | /***/
89 | })
90 | class HelloService {
91 | @Subscription(() => [String], {
92 | nullable: false,
93 | })
94 | // eslint-disable-next-line @typescript-eslint/no-empty-function
95 | hi() {}
96 | }
97 | const context = await Test.createTestingModule({
98 | imports: [sdkModule],
99 | providers: [HelloService],
100 | }).compile();
101 | const app = context.createNestApplication();
102 | const helloService = app.get(HelloService);
103 |
104 | // when
105 | helloService.hi();
106 |
107 | //then
108 | expect(exporterSpy).toHaveBeenCalledWith(
109 | expect.objectContaining({ name: 'Resolver->HelloService.hi' }),
110 | expect.any(Object),
111 | );
112 |
113 | await app.close();
114 | });
115 |
116 | it(`should not trace already tracing prototype`, async () => {
117 | // given
118 | @Injectable()
119 | @Resolver()
120 | class HelloService {
121 | @Query(() => [String], {
122 | nullable: false,
123 | })
124 | // eslint-disable-next-line @typescript-eslint/no-empty-function
125 | hi() {}
126 | }
127 | Reflect.defineMetadata(
128 | Constants.TRACE_METADATA_ACTIVE,
129 | 1,
130 | HelloService.prototype.hi,
131 | );
132 |
133 | const context = await Test.createTestingModule({
134 | imports: [sdkModule],
135 | providers: [HelloService],
136 | }).compile();
137 | const app = context.createNestApplication();
138 | const helloService = app.get(HelloService);
139 |
140 | // when
141 | helloService.hi();
142 |
143 | //then
144 | expect(exporterSpy).not.toHaveBeenCalledWith(
145 | expect.objectContaining({ name: 'Provider->HelloService.hi' }),
146 | expect.any(Object),
147 | );
148 |
149 | await app.close();
150 | });
151 | });
152 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/GraphQLResolverInjector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { ModulesContainer } from '@nestjs/core';
3 | import {
4 | RESOLVER_NAME_METADATA,
5 | RESOLVER_TYPE_METADATA,
6 | } from '@nestjs/graphql/dist/graphql.constants';
7 | import { BaseTraceInjector } from './BaseTraceInjector';
8 | import { Injector } from './Injector';
9 |
10 | @Injectable()
11 | export class GraphQLResolverInjector
12 | extends BaseTraceInjector
13 | implements Injector
14 | {
15 | private readonly loggerService = new Logger();
16 |
17 | constructor(protected readonly modulesContainer: ModulesContainer) {
18 | super(modulesContainer);
19 | }
20 |
21 | public inject() {
22 | const providers = this.getProviders();
23 | for (const provider of providers) {
24 | const isGraphQlResolver = Reflect.hasMetadata(
25 | RESOLVER_NAME_METADATA,
26 | provider.metatype,
27 | );
28 | const keys = this.metadataScanner.getAllMethodNames(
29 | provider.metatype.prototype,
30 | );
31 |
32 | for (const key of keys) {
33 | const resolverMeta = Reflect.getMetadata(
34 | RESOLVER_TYPE_METADATA,
35 | provider.metatype.prototype[key],
36 | );
37 |
38 | const isQueryMutationOrSubscription = [
39 | 'Query',
40 | 'Mutation',
41 | 'Subscription',
42 | ].includes(resolverMeta);
43 | if (
44 | isGraphQlResolver &&
45 | isQueryMutationOrSubscription &&
46 | !this.isAffected(provider.metatype.prototype[key])
47 | ) {
48 | const traceName = `Resolver->${provider.name}.${provider.metatype.prototype[key].name}`;
49 |
50 | provider.metatype.prototype[key] = this.wrap(
51 | provider.metatype.prototype[key],
52 | traceName,
53 | );
54 | this.loggerService.log(
55 | `Mapped ${provider.name}.${key}`,
56 | this.constructor.name,
57 | );
58 | }
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/GuardInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import { CanActivate, Controller, Get, UseGuards } from '@nestjs/common';
5 | import * as request from 'supertest';
6 | import { GuardInjector } from './GuardInjector';
7 | import { APP_GUARD } from '@nestjs/core';
8 | import { Span } from '../Decorators/Span';
9 | import { Tracing } from '../../Tracing';
10 |
11 | describe('Tracing Guard Injector Test', () => {
12 | const sdkModule = OpenTelemetryModule.forRoot([GuardInjector]);
13 | let exporterSpy: jest.SpyInstance;
14 | const exporter = new NoopSpanProcessor();
15 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
16 |
17 | beforeEach(() => {
18 | exporterSpy = jest.spyOn(exporter, 'onStart');
19 | });
20 |
21 | afterEach(() => {
22 | exporterSpy.mockClear();
23 | exporterSpy.mockReset();
24 | });
25 |
26 | it(`should trace guarded controller`, async () => {
27 | // given
28 | class VeyselEfendi implements CanActivate {
29 | canActivate() {
30 | return true;
31 | }
32 | }
33 |
34 | @UseGuards(VeyselEfendi)
35 | @Controller('hello')
36 | class HelloController {
37 | @Get()
38 | // eslint-disable-next-line @typescript-eslint/no-empty-function
39 | hi() {}
40 | }
41 | const context = await Test.createTestingModule({
42 | imports: [sdkModule],
43 | controllers: [HelloController],
44 | }).compile();
45 | const app = context.createNestApplication();
46 | await app.init();
47 |
48 | // when
49 | await request(app.getHttpServer()).get('/hello').send().expect(200);
50 |
51 | //then
52 | expect(exporterSpy).toHaveBeenCalledWith(
53 | expect.objectContaining({ name: 'Guard->HelloController.VeyselEfendi' }),
54 | expect.any(Object),
55 | );
56 |
57 | await app.close();
58 | });
59 |
60 | it(`should trace guarded controller method`, async () => {
61 | // given
62 | class VeyselEfendi implements CanActivate {
63 | canActivate() {
64 | return true;
65 | }
66 | }
67 |
68 | @Controller('hello')
69 | class HelloController {
70 | @Get()
71 | @UseGuards(VeyselEfendi)
72 | // eslint-disable-next-line @typescript-eslint/no-empty-function
73 | hi() {}
74 | }
75 | const context = await Test.createTestingModule({
76 | imports: [sdkModule],
77 | controllers: [HelloController],
78 | }).compile();
79 | const app = context.createNestApplication();
80 | await app.init();
81 |
82 | // when
83 | await request(app.getHttpServer()).get('/hello').send().expect(200);
84 |
85 | //then
86 | expect(exporterSpy).toHaveBeenCalledWith(
87 | expect.objectContaining({
88 | name: 'Guard->HelloController.hi.VeyselEfendi',
89 | }),
90 | expect.any(Object),
91 | );
92 |
93 | await app.close();
94 | });
95 |
96 | it(`should trace guarded and decorated controller method`, async () => {
97 | // given
98 | class VeyselEfendi implements CanActivate {
99 | canActivate() {
100 | return true;
101 | }
102 | }
103 |
104 | @Controller('hello')
105 | class HelloController {
106 | @Get()
107 | @Span('comolokko')
108 | @UseGuards(VeyselEfendi)
109 | // eslint-disable-next-line @typescript-eslint/no-empty-function
110 | hi() {}
111 | }
112 | const context = await Test.createTestingModule({
113 | imports: [sdkModule],
114 | controllers: [HelloController],
115 | }).compile();
116 | const app = context.createNestApplication();
117 | await app.init();
118 |
119 | // when
120 | await request(app.getHttpServer()).get('/hello').send().expect(200);
121 |
122 | //then
123 | expect(exporterSpy).toHaveBeenCalledWith(
124 | expect.objectContaining({
125 | name: 'Guard->HelloController.hi.VeyselEfendi',
126 | }),
127 | expect.any(Object),
128 | );
129 |
130 | await app.close();
131 | });
132 |
133 | it(`should trace global guard`, async () => {
134 | // given
135 | class VeyselEfendi implements CanActivate {
136 | canActivate() {
137 | return true;
138 | }
139 | }
140 | @Controller('hello')
141 | class HelloController {
142 | @Get()
143 | // eslint-disable-next-line @typescript-eslint/no-empty-function
144 | hi() {}
145 | }
146 | const context = await Test.createTestingModule({
147 | imports: [sdkModule],
148 | controllers: [HelloController],
149 | providers: [
150 | {
151 | provide: APP_GUARD,
152 | useClass: VeyselEfendi,
153 | },
154 | ],
155 | }).compile();
156 | const app = context.createNestApplication();
157 | await app.init();
158 |
159 | // when
160 | await request(app.getHttpServer()).get('/hello').send().expect(200);
161 |
162 | //then
163 | expect(exporterSpy).toHaveBeenCalledWith(
164 | expect.objectContaining({ name: 'Guard->Global->VeyselEfendi' }),
165 | expect.any(Object),
166 | );
167 |
168 | await app.close();
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/GuardInjector.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, Injectable, Logger } from '@nestjs/common';
2 | import { Injector } from './Injector';
3 | import { APP_GUARD, ModulesContainer } from '@nestjs/core';
4 | import { BaseTraceInjector } from './BaseTraceInjector';
5 | import { GUARDS_METADATA } from '@nestjs/common/constants';
6 |
7 | @Injectable()
8 | export class GuardInjector extends BaseTraceInjector implements Injector {
9 | private readonly loggerService = new Logger();
10 |
11 | constructor(protected readonly modulesContainer: ModulesContainer) {
12 | super(modulesContainer);
13 | }
14 |
15 | public inject() {
16 | const controllers = this.getControllers();
17 |
18 | for (const controller of controllers) {
19 | if (this.isGuarded(controller.metatype)) {
20 | const guards = this.getGuards(controller.metatype).map((guard) => {
21 | const prototype = guard['prototype'] ?? guard;
22 | const traceName = `Guard->${controller.name}.${prototype.constructor.name}`;
23 | prototype.canActivate = this.wrap(prototype.canActivate, traceName, {
24 | controller: controller.name,
25 | guard: prototype.constructor.name,
26 | scope: 'CONTROLLER',
27 | });
28 | Object.assign(prototype, this);
29 | this.loggerService.log(`Mapped ${traceName}`, this.constructor.name);
30 | return guard;
31 | });
32 |
33 | if (guards.length > 0) {
34 | Reflect.defineMetadata(GUARDS_METADATA, guards, controller.metatype);
35 | }
36 | }
37 |
38 | const keys = this.metadataScanner.getAllMethodNames(
39 | controller.metatype.prototype,
40 | );
41 |
42 | for (const key of keys) {
43 | if (this.isGuarded(controller.metatype.prototype[key])) {
44 | const guards = this.getGuards(controller.metatype.prototype[key]).map(
45 | (guard) => {
46 | const prototype = guard['prototype'] ?? guard;
47 | const traceName = `Guard->${controller.name}.${controller.metatype.prototype[key].name}.${prototype.constructor.name}`;
48 | prototype.canActivate = this.wrap(
49 | prototype.canActivate,
50 | traceName,
51 | {
52 | controller: controller.name,
53 | guard: prototype.constructor.name,
54 | method: controller.metatype.prototype[key].name,
55 | scope: 'CONTROLLER_METHOD',
56 | },
57 | );
58 | Object.assign(prototype, this);
59 | this.loggerService.log(
60 | `Mapped ${traceName}`,
61 | this.constructor.name,
62 | );
63 | return guard;
64 | },
65 | );
66 |
67 | if (guards.length > 0) {
68 | Reflect.defineMetadata(
69 | GUARDS_METADATA,
70 | guards,
71 | controller.metatype.prototype[key],
72 | );
73 | }
74 | }
75 | }
76 | }
77 |
78 | this.injectGlobals();
79 | }
80 |
81 | private injectGlobals() {
82 | const providers = this.getProviders();
83 |
84 | for (const provider of providers) {
85 | if (
86 | typeof provider.token === 'string' &&
87 | provider.token.includes(APP_GUARD) &&
88 | !this.isAffected(provider.metatype.prototype.canActivate)
89 | ) {
90 | const traceName = `Guard->Global->${provider.metatype.name}`;
91 | provider.metatype.prototype.canActivate = this.wrap(
92 | provider.metatype.prototype.canActivate,
93 | traceName,
94 | {
95 | guard: provider.metatype.name,
96 | scope: 'GLOBAL',
97 | },
98 | );
99 | Object.assign(provider.metatype.prototype, this);
100 | this.loggerService.log(`Mapped ${traceName}`, this.constructor.name);
101 | }
102 | }
103 | }
104 |
105 | private getGuards(prototype): CanActivate[] {
106 | return Reflect.getMetadata(GUARDS_METADATA, prototype) || [];
107 | }
108 |
109 | private isGuarded(prototype): boolean {
110 | return Reflect.hasMetadata(GUARDS_METADATA, prototype);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/Injector.ts:
--------------------------------------------------------------------------------
1 | export interface Injector {
2 | inject();
3 | }
4 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/PipeInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import { Controller, Get, PipeTransform, UsePipes } from '@nestjs/common';
5 | import { PipeInjector } from './PipeInjector';
6 | import { PIPES_METADATA } from '@nestjs/common/constants';
7 | import { APP_PIPE } from '@nestjs/core';
8 | import { Tracing } from '../../Tracing';
9 |
10 | describe('Tracing Pipe Injector Test', () => {
11 | const sdkModule = OpenTelemetryModule.forRoot([PipeInjector]);
12 | let exporterSpy: jest.SpyInstance;
13 | const exporter = new NoopSpanProcessor();
14 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
15 |
16 | beforeEach(() => {
17 | exporterSpy = jest.spyOn(exporter, 'onStart');
18 | });
19 |
20 | afterEach(() => {
21 | exporterSpy.mockClear();
22 | exporterSpy.mockReset();
23 | });
24 |
25 | it(`should trace global pipe`, async function () {
26 | // given
27 | class HelloPipe implements PipeTransform {
28 | // eslint-disable-next-line @typescript-eslint/no-empty-function
29 | async transform() {}
30 | }
31 | const context = await Test.createTestingModule({
32 | imports: [sdkModule],
33 | providers: [{ provide: APP_PIPE, useClass: HelloPipe }],
34 | }).compile();
35 | const app = context.createNestApplication();
36 | await app.init();
37 | const injector = app.get(PipeInjector);
38 |
39 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
40 | // @ts-ignore
41 | const providers = injector.getProviders();
42 |
43 | // when
44 | for await (const provider of providers) {
45 | if (
46 | typeof provider.token === 'string' &&
47 | provider.token.includes(APP_PIPE)
48 | ) {
49 | await provider.metatype.prototype.transform(1);
50 | }
51 | }
52 |
53 | //then
54 | expect(exporterSpy).toHaveBeenCalledWith(
55 | expect.objectContaining({ name: 'Pipe->Global->HelloPipe' }),
56 | expect.any(Object),
57 | );
58 |
59 | await app.close();
60 | });
61 |
62 | it(`should trace controller pipe`, async function () {
63 | // given
64 | class HelloPipe implements PipeTransform {
65 | // eslint-disable-next-line @typescript-eslint/no-empty-function
66 | async transform() {}
67 | }
68 |
69 | @Controller('hello')
70 | class HelloController {
71 | @Get()
72 | @UsePipes(HelloPipe)
73 | // eslint-disable-next-line @typescript-eslint/no-empty-function
74 | async hi() {}
75 | }
76 | const context = await Test.createTestingModule({
77 | imports: [sdkModule],
78 | controllers: [HelloController],
79 | }).compile();
80 | const app = context.createNestApplication();
81 | const helloController = app.get(HelloController);
82 | await app.init();
83 |
84 | // when
85 | const pipes = Reflect.getMetadata(PIPES_METADATA, helloController.hi);
86 | await pipes[0].transform(1);
87 |
88 | //then
89 | expect(exporterSpy).toHaveBeenCalledWith(
90 | expect.objectContaining({ name: 'Pipe->HelloController.hi.HelloPipe' }),
91 | expect.any(Object),
92 | );
93 |
94 | await app.close();
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/PipeInjector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger, PipeTransform } from '@nestjs/common';
2 | import { Injector } from './Injector';
3 | import { APP_PIPE, ModulesContainer } from '@nestjs/core';
4 | import { BaseTraceInjector } from './BaseTraceInjector';
5 | import { PIPES_METADATA } from '@nestjs/common/constants';
6 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
7 |
8 | @Injectable()
9 | export class PipeInjector extends BaseTraceInjector implements Injector {
10 | private readonly loggerService = new Logger();
11 |
12 | constructor(protected readonly modulesContainer: ModulesContainer) {
13 | super(modulesContainer);
14 | }
15 |
16 | public inject() {
17 | const controllers = this.getControllers();
18 |
19 | for (const controller of controllers) {
20 | const keys = this.metadataScanner.getAllMethodNames(
21 | controller.metatype.prototype,
22 | );
23 |
24 | for (const key of keys) {
25 | if (this.isPath(controller.metatype.prototype[key])) {
26 | const pipes = this.getPipes(controller.metatype.prototype[key]).map(
27 | (pipe) =>
28 | this.wrapPipe(
29 | pipe,
30 | controller,
31 | controller.metatype.prototype[key],
32 | ),
33 | );
34 |
35 | if (pipes.length > 0) {
36 | Reflect.defineMetadata(
37 | PIPES_METADATA,
38 | pipes,
39 | controller.metatype.prototype[key],
40 | );
41 | }
42 | }
43 | }
44 | }
45 |
46 | this.injectGlobals();
47 | }
48 |
49 | private injectGlobals() {
50 | const providers = this.getProviders();
51 |
52 | for (const provider of providers) {
53 | if (
54 | typeof provider.token === 'string' &&
55 | provider.token.includes(APP_PIPE) &&
56 | !this.isAffected(provider.metatype.prototype.transform)
57 | ) {
58 | const traceName = `Pipe->Global->${provider.metatype.name}`;
59 | provider.metatype.prototype.transform = this.wrap(
60 | provider.metatype.prototype.transform,
61 | traceName,
62 | {
63 | pipe: provider.metatype.name,
64 | scope: 'GLOBAL',
65 | },
66 | );
67 | this.loggerService.log(`Mapped ${traceName}`, this.constructor.name);
68 | }
69 | }
70 | }
71 |
72 | private wrapPipe(
73 | pipe: PipeTransform,
74 | controller: InstanceWrapper,
75 | prototype,
76 | ): PipeTransform {
77 | const pipeProto = pipe['prototype'] ?? pipe;
78 | if (this.isAffected(pipeProto.transform)) return pipe;
79 |
80 | const traceName = `Pipe->${controller.name}.${prototype.name}.${pipeProto.constructor.name}`;
81 | pipeProto.transform = this.wrap(pipeProto.transform, traceName, {
82 | controller: controller.name,
83 | method: prototype.name,
84 | pipe: pipeProto.constructor.name,
85 | scope: 'METHOD',
86 | });
87 | this.loggerService.log(`Mapped ${traceName}`, this.constructor.name);
88 | return pipeProto;
89 | }
90 |
91 | private getPipes(prototype): PipeTransform[] {
92 | return Reflect.getMetadata(PIPES_METADATA, prototype) || [];
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/ScheduleInjector.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { Injector } from './Injector';
3 | import { ModulesContainer } from '@nestjs/core';
4 | import { BaseTraceInjector } from './BaseTraceInjector';
5 |
6 | @Injectable()
7 | export class ScheduleInjector extends BaseTraceInjector implements Injector {
8 | private static SCHEDULE_CRON_OPTIONS = 'SCHEDULE_CRON_OPTIONS';
9 | private static SCHEDULE_INTERVAL_OPTIONS = 'SCHEDULE_INTERVAL_OPTIONS';
10 | private static SCHEDULE_TIMEOUT_OPTIONS = 'SCHEDULE_TIMEOUT_OPTIONS';
11 | private static SCHEDULER_NAME = 'SCHEDULER_NAME';
12 |
13 | private readonly loggerService = new Logger();
14 |
15 | constructor(protected readonly modulesContainer: ModulesContainer) {
16 | super(modulesContainer);
17 | }
18 |
19 | public inject() {
20 | const providers = this.getProviders();
21 |
22 | for (const provider of providers) {
23 | const keys = this.metadataScanner.getAllMethodNames(
24 | provider.metatype.prototype,
25 | );
26 |
27 | for (const key of keys) {
28 | if (
29 | !this.isDecorated(provider.metatype.prototype[key]) &&
30 | !this.isAffected(provider.metatype.prototype[key]) &&
31 | this.isScheduler(provider.metatype.prototype[key])
32 | ) {
33 | const name = this.getName(provider, provider.metatype.prototype[key]);
34 | provider.metatype.prototype[key] = this.wrap(
35 | provider.metatype.prototype[key],
36 | name,
37 | );
38 | this.loggerService.log(`Mapped ${name}`, this.constructor.name);
39 | }
40 | }
41 | }
42 | }
43 |
44 | private isScheduler(prototype): boolean {
45 | return (
46 | this.isCron(prototype) ||
47 | this.isTimeout(prototype) ||
48 | this.isInterval(prototype)
49 | );
50 | }
51 |
52 | private isCron(prototype): boolean {
53 | return Reflect.hasMetadata(
54 | ScheduleInjector.SCHEDULE_CRON_OPTIONS,
55 | prototype,
56 | );
57 | }
58 |
59 | private isTimeout(prototype): boolean {
60 | return Reflect.hasMetadata(
61 | ScheduleInjector.SCHEDULE_TIMEOUT_OPTIONS,
62 | prototype,
63 | );
64 | }
65 |
66 | private isInterval(prototype): boolean {
67 | return Reflect.hasMetadata(
68 | ScheduleInjector.SCHEDULE_INTERVAL_OPTIONS,
69 | prototype,
70 | );
71 | }
72 |
73 | private getName(provider, prototype): string {
74 | if (this.isCron(prototype)) {
75 | const options = Reflect.getMetadata(
76 | ScheduleInjector.SCHEDULE_CRON_OPTIONS,
77 | prototype,
78 | );
79 | if (options && options.name) {
80 | return `Scheduler->Cron->${provider.name}.${options.name}`;
81 | }
82 | return `Scheduler->Cron->${provider.name}.${prototype.name}`;
83 | }
84 |
85 | if (this.isTimeout(prototype)) {
86 | const name = Reflect.getMetadata(
87 | ScheduleInjector.SCHEDULER_NAME,
88 | prototype,
89 | );
90 | if (name) {
91 | return `Scheduler->Timeout->${provider.name}.${name}`;
92 | }
93 | return `Scheduler->Timeout->${provider.name}.${prototype.name}`;
94 | }
95 |
96 | if (this.isInterval(prototype)) {
97 | const name = Reflect.getMetadata(
98 | ScheduleInjector.SCHEDULER_NAME,
99 | prototype,
100 | );
101 | if (name) {
102 | return `Scheduler->Interval->${provider.name}.${name}`;
103 | }
104 | return `Scheduler->Interval->${provider.name}.${prototype.name}`;
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/Trace/Injectors/SchedulerInjector.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import { OpenTelemetryModule } from '../../OpenTelemetryModule';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import { Injectable } from '@nestjs/common';
5 | import { Span } from '../Decorators/Span';
6 | import { Cron, Interval, Timeout } from '@nestjs/schedule';
7 | import { ScheduleInjector } from './ScheduleInjector';
8 | import { Tracing } from '../../Tracing';
9 |
10 | describe('Tracing Scheduler Injector Test', () => {
11 | const sdkModule = OpenTelemetryModule.forRoot([ScheduleInjector]);
12 | let exporterSpy: jest.SpyInstance;
13 | const exporter = new NoopSpanProcessor();
14 | Tracing.init({ serviceName: 'a', spanProcessor: exporter });
15 |
16 | beforeEach(() => {
17 | exporterSpy = jest.spyOn(exporter, 'onStart');
18 | });
19 |
20 | afterEach(() => {
21 | exporterSpy.mockClear();
22 | exporterSpy.mockReset();
23 | });
24 |
25 | it(`should trace scheduled cron method`, async () => {
26 | // given
27 | @Injectable()
28 | class HelloService {
29 | @Cron('2 * * * * *')
30 | // eslint-disable-next-line @typescript-eslint/no-empty-function
31 | hi() {}
32 | }
33 | const context = await Test.createTestingModule({
34 | imports: [sdkModule],
35 | providers: [HelloService],
36 | }).compile();
37 | const app = context.createNestApplication();
38 | const helloService = app.get(HelloService);
39 | await app.init();
40 |
41 | // when
42 | helloService.hi();
43 |
44 | //then
45 | expect(exporterSpy).toHaveBeenCalledWith(
46 | expect.objectContaining({ name: 'Scheduler->Cron->HelloService.hi' }),
47 | expect.any(Object),
48 | );
49 |
50 | await app.close();
51 | });
52 |
53 | it(`should trace scheduled and named cron method`, async () => {
54 | // given
55 | @Injectable()
56 | class HelloService {
57 | @Cron('2 * * * * *', { name: 'AKSUNGUR' })
58 | // eslint-disable-next-line @typescript-eslint/no-empty-function
59 | hi() {}
60 | }
61 | const context = await Test.createTestingModule({
62 | imports: [sdkModule],
63 | providers: [HelloService],
64 | }).compile();
65 | const app = context.createNestApplication();
66 | const helloService = app.get(HelloService);
67 | await app.init();
68 |
69 | // when
70 | helloService.hi();
71 |
72 | //then
73 | expect(exporterSpy).toHaveBeenCalledWith(
74 | expect.objectContaining({
75 | name: 'Scheduler->Cron->HelloService.AKSUNGUR',
76 | }),
77 | expect.any(Object),
78 | );
79 |
80 | await app.close();
81 | });
82 |
83 | it(`should not trace already decorated cron method`, async () => {
84 | // given
85 | @Injectable()
86 | class HelloService {
87 | @Cron('2 * * * * *')
88 | @Span('ORUC_REIS')
89 | // eslint-disable-next-line @typescript-eslint/no-empty-function
90 | hi() {}
91 | }
92 | const context = await Test.createTestingModule({
93 | imports: [sdkModule],
94 | providers: [HelloService],
95 | }).compile();
96 | const app = context.createNestApplication();
97 | const helloService = app.get(HelloService);
98 | await app.init();
99 |
100 | // when
101 | helloService.hi();
102 |
103 | //then
104 | expect(exporterSpy).toHaveBeenCalledWith(
105 | expect.objectContaining({ name: 'Provider->HelloService.ORUC_REIS' }),
106 | expect.any(Object),
107 | );
108 |
109 | await app.close();
110 | });
111 |
112 | it(`should trace scheduled interval method`, async () => {
113 | // given
114 | @Injectable()
115 | class HelloService {
116 | @Interval(100)
117 | // eslint-disable-next-line @typescript-eslint/no-empty-function
118 | hi() {}
119 | }
120 | const context = await Test.createTestingModule({
121 | imports: [sdkModule],
122 | providers: [HelloService],
123 | }).compile();
124 | const app = context.createNestApplication();
125 | const helloService = app.get(HelloService);
126 | await app.init();
127 |
128 | // when
129 | helloService.hi();
130 |
131 | //then
132 | expect(exporterSpy).toHaveBeenCalledWith(
133 | expect.objectContaining({ name: 'Scheduler->Interval->HelloService.hi' }),
134 | expect.any(Object),
135 | );
136 |
137 | await app.close();
138 | });
139 |
140 | it(`should trace scheduled and named interval method`, async () => {
141 | // given
142 | @Injectable()
143 | class HelloService {
144 | @Interval('FATIH', 100)
145 | // eslint-disable-next-line @typescript-eslint/no-empty-function
146 | hi() {}
147 | }
148 | const context = await Test.createTestingModule({
149 | imports: [sdkModule],
150 | providers: [HelloService],
151 | }).compile();
152 | const app = context.createNestApplication();
153 | const helloService = app.get(HelloService);
154 | await app.init();
155 |
156 | // when
157 | helloService.hi();
158 |
159 | //then
160 | expect(exporterSpy).toHaveBeenCalledWith(
161 | expect.objectContaining({
162 | name: 'Scheduler->Interval->HelloService.FATIH',
163 | }),
164 | expect.any(Object),
165 | );
166 |
167 | await app.close();
168 | });
169 |
170 | it(`should trace scheduled timeout method`, async () => {
171 | // given
172 | @Injectable()
173 | class HelloService {
174 | @Timeout(100)
175 | // eslint-disable-next-line @typescript-eslint/no-empty-function
176 | hi() {}
177 | }
178 | const context = await Test.createTestingModule({
179 | imports: [sdkModule],
180 | providers: [HelloService],
181 | }).compile();
182 | const app = context.createNestApplication();
183 | const helloService = app.get(HelloService);
184 | await app.init();
185 |
186 | // when
187 | helloService.hi();
188 |
189 | //then
190 | expect(exporterSpy).toHaveBeenCalledWith(
191 | expect.objectContaining({ name: 'Scheduler->Timeout->HelloService.hi' }),
192 | expect.any(Object),
193 | );
194 |
195 | await app.close();
196 | });
197 |
198 | it(`should trace scheduled and named timeout method`, async () => {
199 | // given
200 | @Injectable()
201 | class HelloService {
202 | @Timeout('BARBAROS', 100)
203 | // eslint-disable-next-line @typescript-eslint/no-empty-function
204 | hi() {}
205 | }
206 | const context = await Test.createTestingModule({
207 | imports: [sdkModule],
208 | providers: [HelloService],
209 | }).compile();
210 | const app = context.createNestApplication();
211 | await app.init();
212 | const helloService = app.get(HelloService);
213 |
214 | // when
215 | helloService.hi();
216 |
217 | //then
218 | expect(exporterSpy).toHaveBeenCalledWith(
219 | expect.objectContaining({
220 | name: 'Scheduler->Timeout->HelloService.BARBAROS',
221 | }),
222 | expect.any(Object),
223 | );
224 |
225 | await app.close();
226 | });
227 | });
228 |
--------------------------------------------------------------------------------
/src/Trace/Logger.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ILogger {
2 | debug: (message: string, params?: Record) => void;
3 | info: (message: string, params?: Record) => void;
4 | warn: (message: string, params?: Record) => void;
5 | error: (
6 | message: string,
7 | err?: Error,
8 | params?: Record,
9 | ) => void;
10 | }
11 |
12 | export interface LoggerOptions {
13 | componentName: string;
14 | logLevel?: LogLevel;
15 | isProduction: boolean;
16 | metadata?: Record;
17 | }
18 |
19 | export enum LogLevel {
20 | Debug = 'debug',
21 | Info = 'info',
22 | Warn = 'warn',
23 | Error = 'error',
24 | }
25 |
--------------------------------------------------------------------------------
/src/Trace/NoopTraceExporter.ts:
--------------------------------------------------------------------------------
1 | import { SpanExporter } from '@opentelemetry/sdk-trace-node';
2 |
3 | export class NoopTraceExporter implements SpanExporter {
4 | export() {
5 | // noop
6 | }
7 |
8 | shutdown(): Promise {
9 | return Promise.resolve(undefined);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/Trace/TraceService.ts:
--------------------------------------------------------------------------------
1 | import { context, trace, Span } from '@opentelemetry/api';
2 | import { Injectable } from '@nestjs/common';
3 | import { Constants } from '../Constants';
4 |
5 | @Injectable()
6 | export class TraceService {
7 | public getTracer() {
8 | return trace.getTracer(Constants.TRACER_NAME);
9 | }
10 |
11 | public getSpan(): Span {
12 | return trace.getSpan(context.active());
13 | }
14 |
15 | public startSpan(name: string): Span {
16 | const tracer = trace.getTracer(Constants.TRACER_NAME);
17 | return tracer.startSpan(name);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/Trace/TraceWrapper.spec.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { TraceWrapper } from './TraceWrapper';
3 | import { ILogger } from './Logger.interface';
4 |
5 | import 'reflect-metadata';
6 |
7 | export const MockedLogger: ILogger = {
8 | debug: jest.fn(),
9 | info: jest.fn(),
10 | warn: jest.fn(),
11 | error: jest.fn(),
12 | };
13 |
14 | class TestClass {
15 | testMethod(): void {
16 | console.log('test');
17 | }
18 |
19 | async testMethodAsync(
20 | startMessage: string,
21 | endMessage: string,
22 | ): Promise {
23 | console.log(startMessage);
24 | await new Promise((resolve) => setTimeout(resolve, 1));
25 | console.log(endMessage);
26 |
27 | return 'testMethodAsync completed';
28 | }
29 | }
30 |
31 | jest.spyOn(console, 'log').mockImplementation(() => {
32 | return;
33 | });
34 | jest.spyOn(console, 'debug').mockImplementation(() => {
35 | return;
36 | });
37 |
38 | describe('TraceWrapper', () => {
39 | let instance: TestClass;
40 | let loggerMock;
41 |
42 | beforeEach(() => {
43 | jest.clearAllMocks();
44 | });
45 |
46 | describe('trace', () => {
47 | beforeEach(() => {
48 | loggerMock = {
49 | debug: jest.fn(),
50 | };
51 | });
52 |
53 | it('should trace all methods in the class', () => {
54 | instance = new TestClass();
55 |
56 | const wrapSpy = jest.spyOn(TraceWrapper, 'wrap');
57 | const tracedInstance = TraceWrapper.trace(instance, loggerMock);
58 |
59 | expect(tracedInstance).toBe(instance);
60 |
61 | expect(wrapSpy).toHaveBeenNthCalledWith(
62 | 1,
63 | expect.objectContaining({ name: 'testMethod' }),
64 | 'TestClass.testMethod',
65 | { class: 'TestClass', method: 'testMethod' },
66 | );
67 | expect(wrapSpy).toHaveBeenNthCalledWith(
68 | 2,
69 | expect.objectContaining({ name: 'testMethodAsync' }),
70 | 'TestClass.testMethodAsync',
71 | expect.anything(),
72 | );
73 |
74 | expect(tracedInstance.testMethod).toBe(instance.testMethod);
75 | expect(tracedInstance.testMethodAsync).toBe(instance.testMethodAsync);
76 | });
77 |
78 | it('should use console as the default logger', () => {
79 | instance = new TestClass();
80 |
81 | jest.spyOn(TraceWrapper, 'wrap').mockReturnValue(instance.testMethod);
82 | const consoleSpy = jest.spyOn(console, 'debug');
83 |
84 | const tracedInstance = TraceWrapper.trace(instance);
85 |
86 | expect(tracedInstance).toBe(instance);
87 |
88 | expect(consoleSpy).toHaveBeenCalledWith('Mapped TestClass.testMethod', {
89 | class: 'TestClass',
90 | method: 'testMethod',
91 | });
92 | });
93 |
94 | it('should wrap an function transparently', async () => {
95 | const original = new TestClass();
96 | const wrapped = TraceWrapper.trace(original, {
97 | attributes: {},
98 | logger: MockedLogger,
99 | });
100 |
101 | const originalSyncResult = original.testMethod();
102 | const wrappedSyncResult = wrapped.testMethod();
103 | const originalAsyncResult = await original.testMethodAsync(
104 | 'start',
105 | 'end',
106 | );
107 | const wrappedAsyncResult = await wrapped.testMethodAsync('start', 'end');
108 |
109 | expect(originalSyncResult).toStrictEqual(wrappedSyncResult);
110 | expect(originalAsyncResult).toBe(wrappedAsyncResult);
111 | });
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/src/Trace/TraceWrapper.ts:
--------------------------------------------------------------------------------
1 | import { Span, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
2 | import { Constants } from '../Constants';
3 | import { TraceWrapperOptions } from './TraceWrapper.types';
4 | import { MetadataScanner } from '../MetaScanner';
5 |
6 | export class TraceWrapper {
7 | /**
8 | * Trace a class by wrapping all methods in a trace segment
9 | * @param instance Instance of the class to trace
10 | * @param options @type {TraceWrapperOptions} Options for the trace
11 | * @returns The traced instance of the class
12 | */
13 | static trace(instance: T, options?: TraceWrapperOptions): T {
14 | const logger = options?.logger ?? console;
15 | const keys = new MetadataScanner().getAllMethodNames(
16 | instance.constructor.prototype,
17 | );
18 | for (const key of keys) {
19 | const defaultTraceName = `${instance.constructor.name}.${instance[key].name}`;
20 | const method = TraceWrapper.wrap(instance[key], defaultTraceName, {
21 | class: instance.constructor.name,
22 | method: instance[key].name,
23 | ...(options?.attributes ?? {}),
24 | });
25 | TraceWrapper.reDecorate(instance[key], method);
26 |
27 | instance[key] = method;
28 | logger.debug(`Mapped ${instance.constructor.name}.${key}`, {
29 | class: instance.constructor.name,
30 | method: key,
31 | });
32 | }
33 |
34 | return instance;
35 | }
36 |
37 | /**
38 | * Wrap a method in a trace segment
39 | * @param prototype prototype of the method to wrap
40 | * @param traceName Span/Segment name
41 | * @param attributes Additional attributes to add to the span
42 | * @returns The wrapped method
43 | */
44 | static wrap(
45 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
46 | prototype: Record,
47 | traceName: string,
48 | attributes = {},
49 | kind?: SpanKind,
50 | ): Record {
51 | let method;
52 |
53 | if (prototype.constructor.name === 'AsyncFunction') {
54 | method = {
55 | [prototype.name]: async function (...args: unknown[]) {
56 | const tracer = trace.getTracer(Constants.TRACER_NAME);
57 | return await tracer.startActiveSpan(
58 | traceName,
59 | { kind },
60 | async (span) => {
61 | span.setAttributes(attributes);
62 | return prototype
63 | .apply(this, args)
64 | .catch((error) => TraceWrapper.recordException(error, span))
65 | .finally(() => {
66 | span.end();
67 | });
68 | },
69 | );
70 | },
71 | }[prototype.name];
72 | } else {
73 | method = {
74 | [prototype.name]: function (...args: unknown[]) {
75 | const tracer = trace.getTracer(Constants.TRACER_NAME);
76 |
77 | return tracer.startActiveSpan(traceName, { kind }, (span) => {
78 | try {
79 | span.setAttributes(attributes);
80 | return prototype.apply(this, args);
81 | } catch (error) {
82 | TraceWrapper.recordException(error, span);
83 | } finally {
84 | span.end();
85 | }
86 | });
87 | },
88 | }[prototype.name];
89 | }
90 |
91 | Reflect.defineMetadata(Constants.TRACE_METADATA, traceName, method);
92 | TraceWrapper.affect(method);
93 | TraceWrapper.reDecorate(prototype, method);
94 |
95 | return method;
96 | }
97 |
98 | private static reDecorate(source, destination) {
99 | const keys = Reflect.getMetadataKeys(source);
100 |
101 | for (const key of keys) {
102 | const meta = Reflect.getMetadata(key, source);
103 | Reflect.defineMetadata(key, meta, destination);
104 | }
105 | }
106 |
107 | private static recordException(error, span: Span) {
108 | span.recordException(error);
109 | span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
110 | throw error;
111 | }
112 |
113 | private static affect(prototype) {
114 | Reflect.defineMetadata(Constants.TRACE_METADATA_ACTIVE, 1, prototype);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/Trace/TraceWrapper.types.ts:
--------------------------------------------------------------------------------
1 | import { ILogger } from './Logger.interface';
2 |
3 | /**
4 | * Options for the trace
5 | * @param attributes Additional attributes to add to each span generated by the methods of the class
6 | * @param logger Logger to use for debugging logs
7 | */
8 | export interface TraceWrapperOptions {
9 | attributes?: Record;
10 | logger?: ILogger;
11 | }
12 |
--------------------------------------------------------------------------------
/src/Tracing.ts:
--------------------------------------------------------------------------------
1 | import { NodeSDK } from '@opentelemetry/sdk-node';
2 | import { TracingConfig } from './TracingConfig.interface';
3 | import { TracingDefaultConfig } from './TracingConfigDefault';
4 |
5 | export class Tracing {
6 | static init(configuration: TracingConfig): void {
7 | const otelSDK = new NodeSDK({
8 | ...TracingDefaultConfig,
9 | ...configuration,
10 | });
11 | otelSDK.start();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/TracingConfig.interface.ts:
--------------------------------------------------------------------------------
1 | import { NodeSDKConfiguration } from '@opentelemetry/sdk-node';
2 |
3 | export interface TracingConfig extends Partial {
4 | serviceName: string;
5 | }
6 |
--------------------------------------------------------------------------------
/src/TracingConfigDefault.ts:
--------------------------------------------------------------------------------
1 | import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
2 | import { Resource } from '@opentelemetry/resources';
3 | import { NoopSpanProcessor } from '@opentelemetry/sdk-trace-node';
4 | import {
5 | CompositePropagator,
6 | W3CTraceContextPropagator,
7 | } from '@opentelemetry/core';
8 | import { JaegerPropagator } from '@opentelemetry/propagator-jaeger';
9 | import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3';
10 | import {
11 | InstrumentationConfigMap,
12 | getNodeAutoInstrumentations,
13 | } from '@opentelemetry/auto-instrumentations-node';
14 | import { containerDetector } from '@opentelemetry/resource-detector-container';
15 | import { Span } from '@opentelemetry/api';
16 | import { IncomingMessage } from 'http';
17 | import { TracingConfig } from './TracingConfig.interface';
18 |
19 | export const NodeAutoInstrumentationsDefaultConfig = {
20 | '@opentelemetry/instrumentation-fs': {
21 | requireParentSpan: true,
22 | enabled: true,
23 | createHook: (_, { args }) => {
24 | return !args[0].toString().indexOf('node_modules');
25 | },
26 | endHook: (_, { args, span }) => {
27 | span.setAttribute('file', args[0].toString());
28 | },
29 | },
30 | '@opentelemetry/instrumentation-http': {
31 | requireParentforOutgoingSpans: true,
32 | requestHook: (span: Span, request: IncomingMessage) => {
33 | span.updateName(`${request.method} ${request.url}`);
34 | },
35 | enabled: true,
36 | ignoreIncomingRequestHook: (request: IncomingMessage) => {
37 | return (
38 | ['/health', '/_health', '/healthz', 'healthcheck'].includes(
39 | request.url,
40 | ) || request.method === 'OPTIONS'
41 | );
42 | },
43 | },
44 | '@opentelemetry/instrumentation-graphql': {
45 | enabled: true,
46 | mergeItems: true,
47 | ignoreResolveSpans: true,
48 | ignoreTrivialResolveSpans: true,
49 | },
50 | '@opentelemetry/instrumentation-net': {
51 | enabled: false,
52 | },
53 | '@opentelemetry/instrumentation-nestjs-core': {
54 | enabled: false,
55 | },
56 | '@opentelemetry/instrumentation-dns': {
57 | enabled: false,
58 | },
59 | '@opentelemetry/instrumentation-express': {
60 | enabled: false,
61 | },
62 | };
63 |
64 | export const TracingDefaultConfig = {
65 | serviceName: 'UNKNOWN',
66 | autoDetectResources: false,
67 | resourceDetectors: [containerDetector],
68 | contextManager: new AsyncLocalStorageContextManager(),
69 | resource: new Resource({
70 | lib: '@overbit/opentelemetry-nestjs',
71 | }),
72 | instrumentations: [
73 | getNodeAutoInstrumentations(NodeAutoInstrumentationsDefaultConfig),
74 | ],
75 | spanProcessor: new NoopSpanProcessor(),
76 | textMapPropagator: new CompositePropagator({
77 | propagators: [
78 | new JaegerPropagator(),
79 | new W3CTraceContextPropagator(),
80 | new B3Propagator(),
81 | new B3Propagator({
82 | injectEncoding: B3InjectEncoding.MULTI_HEADER,
83 | }),
84 | ],
85 | }),
86 | };
87 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Constants';
2 | export * from './Tracing';
3 | export * from './TracingConfigDefault';
4 | export * from './TracingConfig.interface';
5 | export * from './OpenTelemetryModule';
6 | export * from './OpenTelemetryModuleAsyncOption';
7 | export * from './OpenTelemetryModuleConfigDefault';
8 | export * from './OpenTelemetryModuleConfig.interface';
9 |
10 | // Trace
11 | export * from './Trace/Decorators/Span';
12 | export * from './Trace/Decorators/Traceable';
13 | export * from './Trace/TraceService';
14 | export * from './Trace/TraceWrapper';
15 | export * from './Trace/Injectors/ControllerInjector';
16 | export * from './Trace/Injectors/GraphQLResolverInjector';
17 | export * from './Trace/Injectors/EventEmitterInjector';
18 | export * from './Trace/Injectors/GuardInjector';
19 | export * from './Trace/Injectors/ConsoleLoggerInjector';
20 | export * from './Trace/Injectors/PipeInjector';
21 | export * from './Trace/Injectors/ScheduleInjector';
22 | export * from './Trace/NoopTraceExporter';
23 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "node_modules",
5 | "dist",
6 | "src/**/*.spec.ts"
7 | ],
8 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ESNext",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "skipLibCheck": true,
14 | "incremental": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------