├── .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 | 93 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 107 | 109 | 110 | 111 | 112 | 113 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
keyvaluedescription
98 | traceAutoInjectors 99 | ControllerInjector, GuardInjector, EventEmitterInjector, ScheduleInjector, PipeInjector, LoggerInjectordefault auto trace instrumentations inherited from NodeSDKConfiguration
contextManager AsyncLocalStorageContextManager 106 | default trace context manager inherited from NodeSDKConfiguration 108 |
instrumentations AutoInstrumentations 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-fsignores 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 |
spanProcessor NoopSpanProcessor default spanProcessor inherited from NodeSDKConfiguration
textMapPropagator JaegerPropagator, B3Propagator default textMapPropagator inherited from NodeSDKConfiguration
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 | ![Example trace output](./docs/trace-flow.jpeg) 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 | ![Example trace output](./docs/log.png) 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 | --------------------------------------------------------------------------------