├── .prettierrc ├── tsconfig.build.json ├── nest-cli.json ├── src ├── index.ts ├── injector.interface.ts ├── no-span.decorator.ts ├── span.decorator.ts ├── constants.ts ├── trace.service.ts ├── injector-options.interface.ts ├── datadog-trace-module-options.interface.ts ├── datadog-trace.module.spec.ts ├── datadog-trace.module.ts ├── decorator.injector.ts └── decorator.injector.spec.ts ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── .github └── workflows │ └── pipeline.yml ├── LICENSE ├── .vscode └── settings.json ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trace.service'; 2 | export * from './no-span.decorator'; 3 | export * from './span.decorator'; 4 | export * from './datadog-trace.module'; 5 | -------------------------------------------------------------------------------- /src/injector.interface.ts: -------------------------------------------------------------------------------- 1 | import { InjectorOptions } from './injector-options.interface'; 2 | 3 | export interface Injector { 4 | inject(options: InjectorOptions): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/no-span.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { Constants } from './constants'; 3 | 4 | export const NoSpan = () => SetMetadata(Constants.NO_SPAN_METADATA, true); 5 | -------------------------------------------------------------------------------- /src/span.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { Constants } from './constants'; 3 | 4 | export const Span = (name?: string) => 5 | SetMetadata(Constants.SPAN_METADATA, name); 6 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export enum Constants { 2 | NO_SPAN_METADATA = 'DATADOG_NO_SPAN_METADATA', 3 | SPAN_METADATA = 'DATADOG_SPAN_METADATA', 4 | SPAN_METADATA_ACTIVE = 'DATADOG_SPAN_METADATA_ACTIVE', 5 | TRACE_INJECTORS = 'DATADOG_TRACE_INJECTORS', 6 | } 7 | -------------------------------------------------------------------------------- /src/trace.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import tracer, { Span, Tracer } from 'dd-trace'; 3 | 4 | @Injectable() 5 | export class TraceService { 6 | public getTracer(): Tracer { 7 | return tracer; 8 | } 9 | 10 | public getActiveSpan(): Span | null { 11 | return tracer.scope().active(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /lib 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | -------------------------------------------------------------------------------- /src/injector-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface InjectorOptions { 2 | /** 3 | * if true, automatically add a span to all controllers. 4 | */ 5 | controllers?: boolean; 6 | /** 7 | * if true, automatically add a span to all providers. 8 | */ 9 | providers?: boolean; 10 | /** 11 | * list of controller names to exclude when controllers option is true. 12 | */ 13 | excludeControllers?: string[]; 14 | /** 15 | * list of provider names to exclude when controllers option is true. 16 | */ 17 | excludeProviders?: string[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/datadog-trace-module-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface DatadogTraceModuleOptions { 2 | /** 3 | * if true, automatically add a span to all controllers. 4 | */ 5 | controllers?: boolean; 6 | /** 7 | * if true, automatically add a span to all providers. 8 | */ 9 | providers?: boolean; 10 | /** 11 | * list of controller names to exclude when controllers option is true. 12 | */ 13 | excludeControllers?: string[]; 14 | /** 15 | * list of provider names to exclude when controllers option is true. 16 | */ 17 | excludeProviders?: string[]; 18 | } 19 | -------------------------------------------------------------------------------- /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": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./lib", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build-and-publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out code 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: '20' 18 | registry-url: 'https://registry.npmjs.org/' 19 | 20 | - name: Update package version 21 | run: | 22 | VERSION=$(echo ${{ github.event.release.tag_name }} | sed 's/^v//') 23 | npm version $VERSION --no-git-tag-version || true 24 | 25 | - name: Install dependencies 26 | run: npm install 27 | 28 | - name: Build project 29 | run: npm run build 30 | 31 | - name: Run tests and collect coverage 32 | run: npm run test:cov 33 | 34 | - name: Publish to npm 35 | uses: JS-DevTools/npm-publish@v1 36 | with: 37 | token: ${{secrets.NPM_TOKEN}} 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Wonshik Alex Kim 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.renderWhitespace": "all", 3 | "editor.formatOnPaste": true, 4 | "editor.insertSpaces": false, 5 | "editor.renderControlCharacters": true, 6 | "editor.formatOnSave": true, 7 | "files.trimTrailingWhitespace": true, 8 | "files.trimFinalNewlines": true, 9 | "files.insertFinalNewline": true, 10 | "files.exclude": { 11 | "**/.git": true, 12 | "**/.DS_Store": true, 13 | "**/node_modules": true, 14 | "**/*.d.ts": true, 15 | "lib": true 16 | }, 17 | "search.exclude": { 18 | "**/node_modules": true, 19 | "dist": true, 20 | }, 21 | "typescript.tsdk": "node_modules/typescript/lib", 22 | "npm.exclude": "**/extensions/**", 23 | "npm.packageManager": "npm", 24 | "emmet.excludeLanguages": [], 25 | "typescript.preferences.importModuleSpecifier": "non-relative", 26 | "typescript.preferences.quoteStyle": "single", 27 | "[plaintext]": { 28 | "files.insertFinalNewline": false 29 | }, 30 | "[typescript]": { 31 | "editor.defaultFormatter": "vscode.typescript-language-features", 32 | "editor.formatOnSave": true 33 | }, 34 | "[javascript]": { 35 | "editor.defaultFormatter": "vscode.typescript-language-features", 36 | "editor.formatOnSave": true 37 | }, 38 | "json.maxItemsComputed": 5000 39 | } 40 | -------------------------------------------------------------------------------- /src/datadog-trace.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { DatadogTraceModule } from './datadog-trace.module'; 3 | import { TraceService } from './trace.service'; 4 | import type { DatadogTraceModuleOptions } from './datadog-trace-module-options.interface'; 5 | import { Injectable, Module } from '@nestjs/common'; 6 | 7 | describe('DatadogTraceModule', () => { 8 | it('forRoot should register TraceService, DecoratorInjector and TRACE_INJECTORS provider', async () => { 9 | const options: DatadogTraceModuleOptions = {}; 10 | 11 | const moduleRef = await Test.createTestingModule({ 12 | imports: [DatadogTraceModule.forRoot(options)], 13 | }).compile(); 14 | 15 | // exports 16 | expect(moduleRef.get(TraceService)).toBeInstanceOf(TraceService); 17 | }); 18 | 19 | 20 | it('forRootAsync should resolve options via factory and export TraceService', async () => { 21 | @Injectable() 22 | class CustomService { 23 | getOptions(): DatadogTraceModuleOptions { 24 | return { providers: true }; 25 | } 26 | } 27 | 28 | @Module({ 29 | providers: [CustomService], 30 | exports: [CustomService], 31 | }) 32 | class CustomModule { } 33 | 34 | const moduleRef = await Test.createTestingModule({ 35 | imports: [ 36 | DatadogTraceModule.forRootAsync({ 37 | imports: [CustomModule], 38 | inject: [CustomService], 39 | useFactory: async (customService: CustomService) => { 40 | return await customService.getOptions() 41 | }, 42 | }), 43 | ], 44 | }).compile(); 45 | 46 | expect(moduleRef.get(TraceService)).toBeInstanceOf(TraceService); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/datadog-trace.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, type ModuleMetadata } from '@nestjs/common'; 2 | import { FactoryProvider } from '@nestjs/common/interfaces/modules/provider.interface'; 3 | import { TraceService } from './trace.service'; 4 | import { DecoratorInjector } from './decorator.injector'; 5 | import { Injector } from 'src/injector.interface'; 6 | import { Constants } from './constants'; 7 | import { DatadogTraceModuleOptions } from './datadog-trace-module-options.interface'; 8 | 9 | interface DatadogTraceAsyncModuleOptions extends Pick { 10 | useFactory: (...args: any[]) => Promise | DatadogTraceModuleOptions; 11 | inject: any[]; 12 | } 13 | const DATADOG_TRACE_MODULE_PARAMS = Symbol('DATADOG_TRACE_MODULE_PARAMS'); 14 | 15 | 16 | export class DatadogTraceModule { 17 | static forRoot(options: DatadogTraceModuleOptions = {}): DynamicModule { 18 | return { 19 | global: true, 20 | module: DatadogTraceModule, 21 | providers: [ 22 | TraceService, 23 | DecoratorInjector, 24 | { 25 | provide: Constants.TRACE_INJECTORS, 26 | useFactory: async (...injectors: Injector[]) => { 27 | for await (const injector of injectors) { 28 | if (injector.inject) await injector.inject(options); 29 | } 30 | }, 31 | inject: [DecoratorInjector], 32 | } 33 | ], 34 | exports: [TraceService], 35 | }; 36 | } 37 | 38 | static forRootAsync(options: DatadogTraceAsyncModuleOptions): DynamicModule { 39 | return { 40 | global: true, 41 | module: DatadogTraceModule, 42 | imports: options.imports ?? [], 43 | providers: [ 44 | TraceService, 45 | DecoratorInjector, 46 | { 47 | provide: DATADOG_TRACE_MODULE_PARAMS, 48 | inject: options.inject ?? [], 49 | useFactory: options.useFactory, 50 | }, 51 | { 52 | provide: Constants.TRACE_INJECTORS, 53 | inject: [DATADOG_TRACE_MODULE_PARAMS, DecoratorInjector], 54 | useFactory: async (moduleOptions: DatadogTraceModuleOptions, ...injectors: Injector[]) => { 55 | for await (const injector of injectors) { 56 | if (injector.inject) await injector.inject(moduleOptions); 57 | } 58 | }, 59 | } 60 | ], 61 | exports: [TraceService], 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-ddtrace", 3 | "version": "6.0.0", 4 | "description": "NestJS Datadog Trace Library", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/codebrick-corp/nestjs-ddtrace" 8 | }, 9 | "keywords": [ 10 | "nestjs", 11 | "datadog", 12 | "ddtrace", 13 | "dd-trace", 14 | "nest.js" 15 | ], 16 | "main": "lib/index.js", 17 | "author": "wonshikkim.kr@gmail.com", 18 | "private": false, 19 | "license": "MIT", 20 | "scripts": { 21 | "prepare": "npm run build", 22 | "prebuild": "rimraf lib", 23 | "build": "nest build", 24 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 25 | "start": "nest start", 26 | "start:dev": "nest start --watch", 27 | "start:debug": "nest start --debug --watch", 28 | "start:prod": "node lib/main", 29 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 30 | "test": "jest", 31 | "test:watch": "jest --watch", 32 | "test:cov": "jest --coverage", 33 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand" 34 | }, 35 | "peerDependencies": { 36 | "@nestjs/common": "^11.0.0", 37 | "@nestjs/core": "^11.0.0", 38 | "dd-trace": "^5" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^11.0.2", 42 | "@nestjs/common": "^11.0.6", 43 | "@nestjs/core": "^11.0.6", 44 | "@nestjs/microservices": "^11.0.6", 45 | "@nestjs/platform-express": "^11.0.6", 46 | "@nestjs/schematics": "^11.0.0", 47 | "@nestjs/testing": "^11.0.6", 48 | "@types/jest": "29.5.14", 49 | "@types/node": "^22.12.0", 50 | "@types/supertest": "^6.0.2", 51 | "@typescript-eslint/eslint-plugin": "^8.22.0", 52 | "@typescript-eslint/parser": "^8.22.0", 53 | "eslint": "^9.19.0", 54 | "eslint-config-prettier": "^10.0.1", 55 | "eslint-plugin-prettier": "^5.2.3", 56 | "jest": "29.7.0", 57 | "prettier": "^3.4.2", 58 | "reflect-metadata": "^0.2.2", 59 | "rimraf": "^6.0.1", 60 | "source-map-support": "^0.5.21", 61 | "supertest": "^7.0.0", 62 | "ts-jest": "29.2.5", 63 | "ts-loader": "^9.5.2", 64 | "ts-node": "^10.9.2", 65 | "tsconfig-paths": "4.2.0", 66 | "typescript": "^5.7.3" 67 | }, 68 | "jest": { 69 | "moduleFileExtensions": [ 70 | "js", 71 | "json", 72 | "ts" 73 | ], 74 | "rootDir": "src", 75 | "testRegex": ".*\\.spec\\.ts$", 76 | "transform": { 77 | "^.+\\.(t|j)s$": "ts-jest" 78 | }, 79 | "collectCoverageFrom": [ 80 | "**/*.(t|j)s" 81 | ], 82 | "coverageDirectory": "../coverage", 83 | "testEnvironment": "node" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS Datadog Trace 2 | 3 | ## Install 4 | 5 | ```sh 6 | npm i nestjs-ddtrace --save 7 | ``` 8 | 9 | ## Setup 10 | 11 | 1. Create tracing file (tracing.ts): 12 | 13 | ```ts 14 | import tracer from 'dd-trace'; 15 | 16 | // initialized in a different file to avoid hoisting. 17 | tracer.init({ 18 | // https://docs.datadoghq.com/tracing/connect_logs_and_traces/nodejs/ 19 | logInjection: true 20 | }); 21 | export default tracer; 22 | 23 | ``` 24 | 25 | 2. Import the tracing file: 26 | 27 | ```ts 28 | import './tracing'; 29 | import { NestFactory } from '@nestjs/core'; 30 | import { AppModule } from './app.module'; 31 | import { Logger as PinoLogger } from 'nestjs-pino'; 32 | import { Logger } from '@nestjs/common'; 33 | 34 | async function bootstrap() { 35 | const app = await NestFactory.create(AppModule, { bufferLogs: true }); 36 | app.useLogger(app.get(PinoLogger)); 37 | 38 | const logger = new Logger('main'); 39 | const port = process.env.PORT || 3000; 40 | await app.listen(3000).then(() => { 41 | logger.log(`Listening on port: ${port}`); 42 | }); 43 | } 44 | bootstrap(); 45 | ``` 46 | 47 | 3. Add *LoggerModule* and *DatadogModule* to *AppModule*: 48 | 49 | ```ts 50 | import { Module } from '@nestjs/common'; 51 | import { LoggerModule } from 'nestjs-pino'; 52 | import { DatadogTraceModule } from 'nestjs-ddtrace'; 53 | 54 | @Module({ 55 | imports: [LoggerModule.forRoot({ 56 | pinoHttp: { 57 | level: process.env.ENV !== 'prod' ? 'trace' : 'info' 58 | } 59 | }), DatadogTraceModule.forRoot()], 60 | }) 61 | export class AppModule {} 62 | ``` 63 | 64 | ## Span Decorator 65 | 66 | If you need, you can define a custom Tracing Span for a method or class. It works async or sync. Span takes its name from the parameter; but by default, it is the same as the method's name. 67 | 68 | ```ts 69 | import { DatadogTraceModule } from 'nestjs-ddtrace'; 70 | 71 | @Module({ 72 | imports: [DatadogTraceModule.forRoot()], 73 | }) 74 | 75 | export class AppModule {} 76 | ``` 77 | 78 | ### Tracing Service 79 | 80 | In case you need to access native span methods for special logics in the method block: 81 | 82 | ```ts 83 | import { Span, TraceService } from 'nestjs-ddtrace'; 84 | 85 | @Injectable() 86 | export class BookService { 87 | constructor(private readonly traceService: TraceService) {} 88 | 89 | @Span() 90 | async getBooks() { 91 | const currentSpan = this.traceService.getActiveSpan(); // --> retrives current span, comes from http or @Span 92 | await this.doSomething(); 93 | currentSpan.addTags({ 94 | 'getBooks': 'true' 95 | }); 96 | 97 | const childSpan = this.traceService.getTracer().startSpan('ms', {childOf: currentSpan}); 98 | childSpan.setTag('userId', 1); 99 | await this.doSomethingElse(); 100 | childSpan.finish(); // new span ends 101 | 102 | try { 103 | doSomething(); 104 | } catch (e) { 105 | currentSpan.setTag('error', e); 106 | throw e; 107 | } 108 | return [`Harry Potter and the Philosopher's Stone`]; 109 | } 110 | } 111 | ``` 112 | 113 | ```ts 114 | import { Span } from 'nestjs-ddtrace'; 115 | 116 | @Injectable() 117 | @Span() 118 | export class BookService { 119 | async getBooks() { ... } 120 | async deleteBook(id: string) { ... } 121 | } 122 | 123 | @Controller() 124 | @Span() 125 | export class HelloController { 126 | @Get('/books') 127 | getBooks() { ... } 128 | 129 | @Delete('/books/:id') 130 | deleteBooks() { ... } 131 | } 132 | ``` 133 | 134 | ## No Span Decorator 135 | 136 | If you need to explicitly exclude a method or class from having a custom tracing Span then 137 | you can explicitly exclude it. 138 | 139 | ```ts 140 | import { NoSpan, Span } from 'nestjs-ddtrace'; 141 | 142 | @Injectable() 143 | @Span() 144 | export class BookService { 145 | async getBooks() { ... } 146 | @NoSpan() 147 | async deleteBook(id: string) { ... } 148 | } 149 | 150 | @Controller() 151 | @NoSpan() 152 | export class HelloController { 153 | @Get('/books') 154 | getBooks() { ... } 155 | 156 | @Delete('/books/:id') 157 | deleteBooks() { ... } 158 | } 159 | ``` 160 | 161 | ## Custom tracing spans for all controllers and providers 162 | 163 | Custom tracing spans can be enabled for all controllers 164 | and providers using the `controllers` and `providers` options. 165 | 166 | ```ts 167 | import { DatadogTraceModule } from 'nestjs-ddtrace'; 168 | 169 | @Module({ 170 | imports: [DatadogTraceModule.forRoot({ 171 | controllers: true, 172 | providers: true, 173 | })], 174 | }) 175 | 176 | export class AppModule {} 177 | ``` 178 | 179 | Controllers and providers can be excluded by including their name in 180 | either the `excludeControllers` or `excludeProviders` options. 181 | 182 | This may be useful for: 183 | 184 | - having a single place to specify what should be excluded 185 | - excluding controllers and providers you do not own so using the `@NoSpan` decorator is not an option. 186 | 187 | ```ts 188 | import { DatadogTraceModule } from 'nestjs-ddtrace'; 189 | 190 | @Module({ 191 | imports: [DatadogTraceModule.forRoot({ 192 | controllers: true, 193 | providers: true, 194 | excludeProviders: ['TraceService'], 195 | })], 196 | }) 197 | 198 | export class AppModule {} 199 | ``` 200 | 201 | ## Async loading of options 202 | ```ts 203 | import { DatadogTraceModule } from 'nestjs-ddtrace'; 204 | 205 | @Module({ 206 | imports: [ 207 | DatadogTraceModule.forRootAsync({ 208 | imports: [CustomModule], 209 | injects: [CustonService], 210 | useFactory: async (customService: CustomService) => { 211 | return await customService.getOptions(); 212 | } 213 | }) 214 | ], 215 | }) 216 | 217 | export class AppModule {} 218 | ``` 219 | 220 | ## Miscellaneous 221 | 222 | Inspired by the [nestjs-otel](https://github.com/pragmaticivan/nestjs-otel) and [nestjs-opentelemetry](https://github.com/MetinSeylan/Nestjs-OpenTelemetry#readme) repository. 223 | -------------------------------------------------------------------------------- /src/decorator.injector.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 3 | import { Constants } from './constants'; 4 | import { MetadataScanner, ModulesContainer } from '@nestjs/core'; 5 | import { 6 | Controller, 7 | Injectable as InjectableInterface, 8 | } from '@nestjs/common/interfaces'; 9 | import tracer, { Span } from 'dd-trace'; 10 | import { Injector } from './injector.interface'; 11 | import { InjectorOptions } from 'src/injector-options.interface'; 12 | 13 | @Injectable() 14 | export class DecoratorInjector implements Injector { 15 | private readonly metadataScanner: MetadataScanner = new MetadataScanner(); 16 | private readonly logger = new Logger(); 17 | 18 | // eslint-disable-next-line prettier/prettier 19 | constructor(private readonly modulesContainer: ModulesContainer) { } 20 | 21 | public inject(options: InjectorOptions) { 22 | this.injectProviders(options.providers, new Set(options.excludeProviders)); 23 | this.injectControllers( 24 | options.controllers, 25 | new Set(options.excludeControllers), 26 | ); 27 | } 28 | 29 | /** 30 | * Returns whether the prototype is annotated with @Span or not. 31 | * @param prototype 32 | * @returns 33 | */ 34 | private isDecorated(prototype): boolean { 35 | return Reflect.hasMetadata(Constants.SPAN_METADATA, prototype); 36 | } 37 | 38 | /** 39 | * Returns whether the prototype is annotated with @NoSpan or not. 40 | * @param prototype 41 | * @returns 42 | */ 43 | private isExcluded(prototype): boolean { 44 | return Reflect.hasMetadata(Constants.NO_SPAN_METADATA, prototype); 45 | } 46 | 47 | /** 48 | * Returns whether the prototype has already applied wrapper or not. 49 | * @param prototype 50 | * @returns 51 | */ 52 | private isAffected(prototype): boolean { 53 | return Reflect.hasMetadata(Constants.SPAN_METADATA_ACTIVE, prototype); 54 | } 55 | 56 | /** 57 | * Returns the span name specified in span annotation. 58 | * @param prototype 59 | * @returns 60 | */ 61 | private getSpanName(prototype): string { 62 | return Reflect.getMetadata(Constants.SPAN_METADATA, prototype); 63 | } 64 | 65 | /** 66 | * Tag the error that occurred in span. 67 | * @param error 68 | * @param span 69 | */ 70 | private static recordException(error, span: Span) { 71 | span.setTag('error', error); 72 | throw error; 73 | } 74 | 75 | /** 76 | * Find providers with span annotation and wrap method. 77 | */ 78 | private injectProviders(injectAll: boolean, exclude: Set) { 79 | const providers = this.getProviders(); 80 | 81 | for (const provider of providers) { 82 | // If no-span annotation is attached to class 83 | // it and its methods are all excluded 84 | if (this.isExcluded(provider.metatype)) { 85 | continue; 86 | } 87 | 88 | const isExcludedFromInjectAll = exclude.has(provider.name); 89 | if (injectAll && !isExcludedFromInjectAll) { 90 | Reflect.defineMetadata(Constants.SPAN_METADATA, 1, provider.metatype); 91 | } 92 | const isProviderDecorated = this.isDecorated(provider.metatype); 93 | const methodNames = this.metadataScanner.getAllFilteredMethodNames( 94 | provider.metatype.prototype, 95 | ); 96 | 97 | for (const methodName of methodNames) { 98 | const method = provider.metatype.prototype[methodName]; 99 | 100 | // Allready applied or method has been excluded so skip 101 | if (this.isAffected(method) || this.isExcluded(method)) { 102 | continue; 103 | } 104 | 105 | // If span annotation is attached to class, @Span is applied to all methods. 106 | if (isProviderDecorated || this.isDecorated(method)) { 107 | const spanName = 108 | this.getSpanName(method) || `${provider.name}.${methodName}`; 109 | provider.metatype.prototype[methodName] = this.wrap(method, spanName); 110 | 111 | this.logger.log( 112 | `Mapped ${provider.name}.${methodName}`, 113 | this.constructor.name, 114 | ); 115 | } 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Find controllers with span annotation and wrap method. 122 | */ 123 | private injectControllers(injectAll: boolean, exclude: Set) { 124 | const controllers = this.getControllers(); 125 | 126 | for (const controller of controllers) { 127 | // If no-span annotation is attached to class 128 | // it and its methods are all excluded 129 | if (this.isExcluded(controller.metatype)) { 130 | continue; 131 | } 132 | 133 | // Excluded from the injectAll option 134 | const isExcludedFromInjectAll = exclude.has(controller.name); 135 | if (injectAll && !isExcludedFromInjectAll) { 136 | Reflect.defineMetadata(Constants.SPAN_METADATA, 1, controller.metatype); 137 | } 138 | const isControllerDecorated = this.isDecorated(controller.metatype); 139 | const methodNames = this.metadataScanner.getAllFilteredMethodNames( 140 | controller.metatype.prototype, 141 | ); 142 | 143 | for (const methodName of methodNames) { 144 | const method = controller.metatype.prototype[methodName]; 145 | 146 | // Allready applied or method has been excluded so skip 147 | if (this.isAffected(method) || this.isExcluded(method)) { 148 | continue; 149 | } 150 | 151 | // If span annotation is attached to class, @Span is applied to all methods. 152 | if (isControllerDecorated || this.isDecorated(method)) { 153 | const spanName = 154 | this.getSpanName(method) || `${controller.name}.${methodName}`; 155 | controller.metatype.prototype[methodName] = this.wrap( 156 | method, 157 | spanName, 158 | ); 159 | 160 | this.logger.log( 161 | `Mapped ${controller.name}.${methodName}`, 162 | this.constructor.name, 163 | ); 164 | } 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * Wrap the method 171 | * @param prototype 172 | * @param spanName 173 | * @returns 174 | */ 175 | private wrap(prototype: Record, spanName: string) { 176 | const method = { 177 | // To keep function.name property 178 | [prototype.name]: function (...args: any[]) { 179 | const activeSpan = tracer.scope().active(); 180 | const span = tracer.startSpan(spanName, { childOf: activeSpan }); 181 | 182 | return tracer.scope().activate(span, () => { 183 | if (prototype.constructor.name === 'AsyncFunction') { 184 | return prototype 185 | .apply(this, args) 186 | .catch((error) => { 187 | DecoratorInjector.recordException(error, span); 188 | }) 189 | .finally(() => span.finish()); 190 | } else { 191 | try { 192 | const result = prototype.apply(this, args); 193 | // This handles the case where a function isn't async but returns a Promise 194 | if (result && typeof result.then === 'function') { 195 | return result 196 | .catch((error) => { 197 | DecoratorInjector.recordException(error, span); 198 | }) 199 | .finally(() => span.finish()); 200 | } 201 | 202 | span.finish(); 203 | return result; 204 | } catch (error) { 205 | DecoratorInjector.recordException(error, span); 206 | span.finish(); 207 | } 208 | } 209 | }); 210 | }, 211 | }[prototype.name]; 212 | 213 | // Reflect.defineMetadata(Constants.SPAN_METADATA, spanName, method); 214 | 215 | // Flag that wrapping is done 216 | Reflect.defineMetadata(Constants.SPAN_METADATA_ACTIVE, 1, prototype); 217 | 218 | // Copy existing metadata 219 | const source = prototype; 220 | const keys = Reflect.getMetadataKeys(source); 221 | 222 | for (const key of keys) { 223 | const meta = Reflect.getMetadata(key, source); 224 | Reflect.defineMetadata(key, meta, method); 225 | } 226 | 227 | return method; 228 | } 229 | 230 | /** 231 | * Get all the controllers in the module container. 232 | */ 233 | private *getControllers(): Generator> { 234 | for (const module of this.modulesContainer.values()) { 235 | for (const controller of module.controllers.values()) { 236 | if (controller && controller.metatype?.prototype) { 237 | yield controller as InstanceWrapper; 238 | } 239 | } 240 | } 241 | } 242 | 243 | /** 244 | * Get all the providers in the module container. 245 | */ 246 | private *getProviders(): Generator> { 247 | for (const module of this.modulesContainer.values()) { 248 | for (const provider of module.providers.values()) { 249 | if (provider && provider.metatype?.prototype) { 250 | yield provider as InstanceWrapper; 251 | } 252 | } 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/decorator.injector.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Controller, Get, Injectable, UsePipes } from '@nestjs/common'; 3 | import { EventPattern, Transport } from '@nestjs/microservices'; 4 | import { DatadogTraceModule } from './datadog-trace.module'; 5 | import { Span } from './span.decorator'; 6 | import { Constants } from './constants'; 7 | import { tracer, Span as TraceSpan, Scope } from 'dd-trace'; 8 | import * as request from 'supertest'; 9 | import { PATH_METADATA, PIPES_METADATA } from '@nestjs/common/constants'; 10 | import { 11 | PATTERN_METADATA, 12 | PATTERN_HANDLER_METADATA, 13 | TRANSPORT_METADATA, 14 | } from '@nestjs/microservices/constants'; 15 | import { PatternHandler } from '@nestjs/microservices/enums/pattern-handler.enum'; 16 | import { NoSpan } from './no-span.decorator'; 17 | 18 | describe('DecoratorInjector', () => { 19 | it('should work with sync function', async () => { 20 | // given 21 | @Injectable() 22 | class HelloService { 23 | @Span('hello') 24 | hi() { 25 | return 0; 26 | } 27 | } 28 | 29 | const module: TestingModule = await Test.createTestingModule({ 30 | imports: [DatadogTraceModule.forRoot()], 31 | providers: [HelloService], 32 | }).compile(); 33 | 34 | const helloService = module.get(HelloService); 35 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 36 | const startSpanSpy = jest 37 | .spyOn(tracer, 'startSpan') 38 | .mockReturnValue(mockSpan); 39 | const scope = { 40 | active: jest.fn(() => null) as any, 41 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 42 | return fn(); 43 | }) as any, 44 | } as Scope; 45 | const scopeSpy = jest 46 | .spyOn(tracer, 'scope') 47 | .mockImplementation(() => scope); 48 | 49 | // when 50 | const result = helloService.hi(); 51 | 52 | // then 53 | expect(result).toBe(0); 54 | expect( 55 | Reflect.getMetadata(Constants.SPAN_METADATA, HelloService.prototype.hi), 56 | ).toBe('hello'); 57 | expect( 58 | Reflect.getMetadata( 59 | Constants.SPAN_METADATA_ACTIVE, 60 | HelloService.prototype.hi, 61 | ), 62 | ).toBe(1); 63 | expect(tracer.startSpan).toHaveBeenCalledWith('hello', { childOf: null }); 64 | expect(tracer.scope().active).toHaveBeenCalled(); 65 | expect(tracer.scope().activate).toHaveBeenCalled(); 66 | expect(mockSpan.finish).toHaveBeenCalled(); 67 | 68 | startSpanSpy.mockClear(); 69 | scopeSpy.mockClear(); 70 | }); 71 | 72 | it('should work with async function', async () => { 73 | // given 74 | @Injectable() 75 | class HelloService { 76 | @Span('hello') 77 | async hi() { 78 | return new Promise((resolve) => { 79 | setTimeout(() => resolve(0), 100); 80 | }); 81 | } 82 | } 83 | 84 | const module: TestingModule = await Test.createTestingModule({ 85 | imports: [DatadogTraceModule.forRoot()], 86 | providers: [HelloService], 87 | }).compile(); 88 | 89 | const helloService = module.get(HelloService); 90 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 91 | const startSpanSpy = jest 92 | .spyOn(tracer, 'startSpan') 93 | .mockReturnValue(mockSpan); 94 | const scope = { 95 | active: jest.fn(() => null) as any, 96 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 97 | return fn(); 98 | }) as any, 99 | } as Scope; 100 | const scopeSpy = jest 101 | .spyOn(tracer, 'scope') 102 | .mockImplementation(() => scope); 103 | 104 | // when 105 | const result = await helloService.hi(); 106 | 107 | // then 108 | expect(result).toBe(0); 109 | expect( 110 | Reflect.getMetadata(Constants.SPAN_METADATA, HelloService.prototype.hi), 111 | ).toBe('hello'); 112 | expect( 113 | Reflect.getMetadata( 114 | Constants.SPAN_METADATA_ACTIVE, 115 | HelloService.prototype.hi, 116 | ), 117 | ).toBe(1); 118 | expect(tracer.startSpan).toHaveBeenCalledWith('hello', { childOf: null }); 119 | expect(tracer.scope().active).toHaveBeenCalled(); 120 | expect(tracer.scope().activate).toHaveBeenCalled(); 121 | expect(mockSpan.finish).toHaveBeenCalled(); 122 | 123 | startSpanSpy.mockClear(); 124 | scopeSpy.mockClear(); 125 | }); 126 | 127 | it('should work with non-async promise function', async () => { 128 | let resolve; 129 | 130 | const promise = new Promise((_resolve) => { 131 | resolve = _resolve; 132 | }); 133 | // given 134 | @Injectable() 135 | class HelloService { 136 | @Span('hello') 137 | hi() { 138 | return promise; 139 | } 140 | } 141 | 142 | const module: TestingModule = await Test.createTestingModule({ 143 | imports: [DatadogTraceModule.forRoot()], 144 | providers: [HelloService], 145 | }).compile(); 146 | 147 | const helloService = module.get(HelloService); 148 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 149 | const startSpanSpy = jest 150 | .spyOn(tracer, 'startSpan') 151 | .mockReturnValue(mockSpan); 152 | const scope = { 153 | active: jest.fn(() => null) as any, 154 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 155 | return fn(); 156 | }) as any, 157 | } as Scope; 158 | const scopeSpy = jest 159 | .spyOn(tracer, 'scope') 160 | .mockImplementation(() => scope); 161 | 162 | // when 163 | const resultPromise = helloService.hi(); 164 | // The span should not be finished before the promise resolves 165 | expect(mockSpan.finish).not.toHaveBeenCalled(); 166 | resolve(0); 167 | const result = await resultPromise; 168 | 169 | // then 170 | expect(result).toBe(0); 171 | expect( 172 | Reflect.getMetadata(Constants.SPAN_METADATA, HelloService.prototype.hi), 173 | ).toBe('hello'); 174 | expect( 175 | Reflect.getMetadata( 176 | Constants.SPAN_METADATA_ACTIVE, 177 | HelloService.prototype.hi, 178 | ), 179 | ).toBe(1); 180 | expect(tracer.startSpan).toHaveBeenCalledWith('hello', { childOf: null }); 181 | expect(tracer.scope().active).toHaveBeenCalled(); 182 | expect(tracer.scope().activate).toHaveBeenCalled(); 183 | expect(mockSpan.finish).toHaveBeenCalled(); 184 | 185 | startSpanSpy.mockClear(); 186 | scopeSpy.mockClear(); 187 | }); 188 | 189 | it('should record exception with sync function', async () => { 190 | // given 191 | @Injectable() 192 | class HelloService { 193 | @Span('hello') 194 | hi() { 195 | throw new Error('hello'); 196 | } 197 | } 198 | 199 | const module: TestingModule = await Test.createTestingModule({ 200 | imports: [DatadogTraceModule.forRoot()], 201 | providers: [HelloService], 202 | }).compile(); 203 | 204 | const helloService = module.get(HelloService); 205 | const mockSpan = { 206 | finish: jest.fn() as any, 207 | setTag: jest.fn() as any, 208 | } as TraceSpan; 209 | const startSpanSpy = jest 210 | .spyOn(tracer, 'startSpan') 211 | .mockReturnValue(mockSpan); 212 | const scope = { 213 | active: jest.fn(() => null) as any, 214 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 215 | return fn(); 216 | }) as any, 217 | } as Scope; 218 | const scopeSpy = jest 219 | .spyOn(tracer, 'scope') 220 | .mockImplementation(() => scope); 221 | 222 | // when 223 | expect(() => { 224 | helloService.hi(); 225 | }).toThrowError('hello'); 226 | 227 | // then 228 | expect( 229 | Reflect.getMetadata(Constants.SPAN_METADATA, HelloService.prototype.hi), 230 | ).toBe('hello'); 231 | expect( 232 | Reflect.getMetadata( 233 | Constants.SPAN_METADATA_ACTIVE, 234 | HelloService.prototype.hi, 235 | ), 236 | ).toBe(1); 237 | expect(tracer.startSpan).toHaveBeenCalledWith('hello', { childOf: null }); 238 | expect(tracer.scope().active).toHaveBeenCalled(); 239 | expect(tracer.scope().activate).toHaveBeenCalled(); 240 | expect(mockSpan.finish).toHaveBeenCalled(); 241 | expect(mockSpan.setTag).toHaveBeenCalledWith('error', new Error('hello')); 242 | 243 | startSpanSpy.mockClear(); 244 | scopeSpy.mockClear(); 245 | }); 246 | 247 | it('should record exception with async function', async () => { 248 | // given 249 | @Injectable() 250 | class HelloService { 251 | @Span('hello') 252 | async hi() { 253 | return new Promise((_resolve, reject) => { 254 | setTimeout(() => reject(new Error('hello')), 100); 255 | }); 256 | } 257 | } 258 | 259 | const module: TestingModule = await Test.createTestingModule({ 260 | imports: [DatadogTraceModule.forRoot()], 261 | providers: [HelloService], 262 | }).compile(); 263 | 264 | const helloService = module.get(HelloService); 265 | const mockSpan = { 266 | finish: jest.fn() as any, 267 | setTag: jest.fn() as any, 268 | } as TraceSpan; 269 | const startSpanSpy = jest 270 | .spyOn(tracer, 'startSpan') 271 | .mockReturnValue(mockSpan); 272 | const scope = { 273 | active: jest.fn(() => null) as any, 274 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 275 | return fn(); 276 | }) as any, 277 | } as Scope; 278 | const scopeSpy = jest 279 | .spyOn(tracer, 'scope') 280 | .mockImplementation(() => scope); 281 | 282 | // when 283 | await expect(helloService.hi()).rejects.toEqual(new Error('hello')); 284 | 285 | // then 286 | expect( 287 | Reflect.getMetadata(Constants.SPAN_METADATA, HelloService.prototype.hi), 288 | ).toBe('hello'); 289 | expect( 290 | Reflect.getMetadata( 291 | Constants.SPAN_METADATA_ACTIVE, 292 | HelloService.prototype.hi, 293 | ), 294 | ).toBe(1); 295 | expect(tracer.startSpan).toHaveBeenCalledWith('hello', { childOf: null }); 296 | expect(tracer.scope().active).toHaveBeenCalled(); 297 | expect(tracer.scope().activate).toHaveBeenCalled(); 298 | expect(mockSpan.finish).toHaveBeenCalled(); 299 | expect(mockSpan.setTag).toHaveBeenCalledWith('error', new Error('hello')); 300 | 301 | startSpanSpy.mockClear(); 302 | scopeSpy.mockClear(); 303 | }); 304 | 305 | it('should work with all methods in provider', async () => { 306 | // given 307 | @Injectable() 308 | @Span() 309 | class HelloService { 310 | hi() { 311 | return 0; 312 | } 313 | hello() { 314 | return 1; 315 | } 316 | } 317 | 318 | const module: TestingModule = await Test.createTestingModule({ 319 | imports: [DatadogTraceModule.forRoot()], 320 | providers: [HelloService], 321 | }).compile(); 322 | 323 | const helloService = module.get(HelloService); 324 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 325 | const startSpanSpy = jest 326 | .spyOn(tracer, 'startSpan') 327 | .mockReturnValue(mockSpan); 328 | const scope = { 329 | active: jest.fn(() => null) as any, 330 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 331 | return fn(); 332 | }) as any, 333 | } as Scope; 334 | const scopeSpy = jest 335 | .spyOn(tracer, 'scope') 336 | .mockImplementation(() => scope); 337 | 338 | // when 339 | const result1 = helloService.hi(); 340 | const result2 = helloService.hello(); 341 | 342 | // then 343 | expect(result1).toBe(0); 344 | expect(result2).toBe(1); 345 | expect( 346 | Reflect.getMetadata( 347 | Constants.SPAN_METADATA_ACTIVE, 348 | HelloService.prototype.hi, 349 | ), 350 | ).toBe(1); 351 | expect( 352 | Reflect.getMetadata( 353 | Constants.SPAN_METADATA_ACTIVE, 354 | HelloService.prototype.hello, 355 | ), 356 | ).toBe(1); 357 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hi', { 358 | childOf: null, 359 | }); 360 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hello', { 361 | childOf: null, 362 | }); 363 | expect(tracer.scope().active).toHaveBeenCalledTimes(2); 364 | expect(tracer.scope().activate).toHaveBeenCalledTimes(2); 365 | expect(mockSpan.finish).toHaveBeenCalledTimes(2); 366 | 367 | startSpanSpy.mockClear(); 368 | scopeSpy.mockClear(); 369 | }); 370 | 371 | it('should exclude provider and all its methods', async () => { 372 | // given 373 | @Injectable() 374 | @NoSpan() 375 | class HelloService { 376 | @Span() 377 | hi() { 378 | return 0; 379 | } 380 | hello() { 381 | return 1; 382 | } 383 | } 384 | 385 | const module: TestingModule = await Test.createTestingModule({ 386 | imports: [DatadogTraceModule.forRoot()], 387 | providers: [HelloService], 388 | }).compile(); 389 | 390 | const helloService = module.get(HelloService); 391 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 392 | const startSpanSpy = jest 393 | .spyOn(tracer, 'startSpan') 394 | .mockReturnValue(mockSpan); 395 | const scope = { 396 | active: jest.fn(() => null) as any, 397 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 398 | return fn(); 399 | }) as any, 400 | } as Scope; 401 | const scopeSpy = jest 402 | .spyOn(tracer, 'scope') 403 | .mockImplementation(() => scope); 404 | 405 | // when 406 | const result1 = helloService.hi(); 407 | const result2 = helloService.hello(); 408 | 409 | // then 410 | expect(result1).toBe(0); 411 | expect(result2).toBe(1); 412 | expect( 413 | Reflect.getMetadata( 414 | Constants.SPAN_METADATA_ACTIVE, 415 | HelloService.prototype.hi, 416 | ), 417 | ).toBeUndefined(); 418 | expect( 419 | Reflect.getMetadata( 420 | Constants.SPAN_METADATA_ACTIVE, 421 | HelloService.prototype.hello, 422 | ), 423 | ).toBeUndefined(); 424 | expect(tracer.startSpan).not.toHaveBeenCalled(); 425 | expect(tracer.scope().active).not.toHaveBeenCalled(); 426 | expect(tracer.scope().activate).not.toHaveBeenCalled(); 427 | expect(mockSpan.finish).not.toHaveBeenCalled(); 428 | 429 | startSpanSpy.mockClear(); 430 | scopeSpy.mockClear(); 431 | }); 432 | 433 | it('should exclude methods in provider', async () => { 434 | // given 435 | @Injectable() 436 | @Span() 437 | class HelloService { 438 | hi() { 439 | return 0; 440 | } 441 | @NoSpan() 442 | hello() { 443 | return 1; 444 | } 445 | } 446 | 447 | const module: TestingModule = await Test.createTestingModule({ 448 | imports: [DatadogTraceModule.forRoot()], 449 | providers: [HelloService], 450 | }).compile(); 451 | 452 | const helloService = module.get(HelloService); 453 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 454 | const startSpanSpy = jest 455 | .spyOn(tracer, 'startSpan') 456 | .mockReturnValue(mockSpan); 457 | const scope = { 458 | active: jest.fn(() => null) as any, 459 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 460 | return fn(); 461 | }) as any, 462 | } as Scope; 463 | const scopeSpy = jest 464 | .spyOn(tracer, 'scope') 465 | .mockImplementation(() => scope); 466 | 467 | // when 468 | const result1 = helloService.hi(); 469 | const result2 = helloService.hello(); 470 | 471 | // then 472 | expect(result1).toBe(0); 473 | expect(result2).toBe(1); 474 | expect( 475 | Reflect.getMetadata( 476 | Constants.SPAN_METADATA_ACTIVE, 477 | HelloService.prototype.hi, 478 | ), 479 | ).toBe(1); 480 | expect( 481 | Reflect.getMetadata( 482 | Constants.SPAN_METADATA_ACTIVE, 483 | HelloService.prototype.hello, 484 | ), 485 | ).toBeUndefined(); 486 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hi', { 487 | childOf: null, 488 | }); 489 | expect(tracer.startSpan).not.toHaveBeenCalledWith('HelloService.hello', { 490 | childOf: null, 491 | }); 492 | expect(tracer.scope().active).toHaveBeenCalledTimes(1); 493 | expect(tracer.scope().activate).toHaveBeenCalledTimes(1); 494 | expect(mockSpan.finish).toHaveBeenCalledTimes(1); 495 | 496 | startSpanSpy.mockClear(); 497 | scopeSpy.mockClear(); 498 | }); 499 | 500 | it('should work with all methods in controller', async () => { 501 | // given 502 | @Controller() 503 | @Span() 504 | class HelloController { 505 | @Get('/hi') 506 | hi() { 507 | return 0; 508 | } 509 | @Get('/hello') 510 | hello() { 511 | return 1; 512 | } 513 | } 514 | 515 | const module: TestingModule = await Test.createTestingModule({ 516 | imports: [DatadogTraceModule.forRoot()], 517 | controllers: [HelloController], 518 | }).compile(); 519 | const app = module.createNestApplication(); 520 | await app.init(); 521 | 522 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 523 | const startSpanSpy = jest 524 | .spyOn(tracer, 'startSpan') 525 | .mockReturnValue(mockSpan); 526 | const scope = { 527 | active: jest.fn(() => null) as any, 528 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 529 | return fn(); 530 | }) as any, 531 | } as Scope; 532 | const scopeSpy = jest 533 | .spyOn(tracer, 'scope') 534 | .mockImplementation(() => scope); 535 | 536 | // when 537 | await request(app.getHttpServer()).get('/hi').send().expect(200); 538 | await request(app.getHttpServer()).get('/hello').send().expect(200); 539 | 540 | // then 541 | expect( 542 | Reflect.getMetadata( 543 | Constants.SPAN_METADATA_ACTIVE, 544 | HelloController.prototype.hi, 545 | ), 546 | ).toBe(1); 547 | expect( 548 | Reflect.getMetadata( 549 | Constants.SPAN_METADATA_ACTIVE, 550 | HelloController.prototype.hello, 551 | ), 552 | ).toBe(1); 553 | expect( 554 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hi), 555 | ).toBe('/hi'); 556 | expect( 557 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hello), 558 | ).toBe('/hello'); 559 | 560 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hi', { 561 | childOf: null, 562 | }); 563 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hello', { 564 | childOf: null, 565 | }); 566 | expect(tracer.scope().active).toHaveBeenCalledTimes(2); 567 | expect(tracer.scope().activate).toHaveBeenCalledTimes(2); 568 | expect(mockSpan.finish).toHaveBeenCalledTimes(2); 569 | 570 | startSpanSpy.mockClear(); 571 | scopeSpy.mockClear(); 572 | }); 573 | 574 | it('should exclude controller and all its methods', async () => { 575 | // given 576 | @Controller() 577 | @NoSpan() 578 | class HelloController { 579 | @Get('/hi') 580 | @Span() 581 | hi() { 582 | return 0; 583 | } 584 | @Get('/hello') 585 | hello() { 586 | return 1; 587 | } 588 | } 589 | 590 | const module: TestingModule = await Test.createTestingModule({ 591 | imports: [DatadogTraceModule.forRoot()], 592 | controllers: [HelloController], 593 | }).compile(); 594 | const app = module.createNestApplication(); 595 | await app.init(); 596 | 597 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 598 | const startSpanSpy = jest 599 | .spyOn(tracer, 'startSpan') 600 | .mockReturnValue(mockSpan); 601 | const scope = { 602 | active: jest.fn(() => null) as any, 603 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 604 | return fn(); 605 | }) as any, 606 | } as Scope; 607 | const scopeSpy = jest 608 | .spyOn(tracer, 'scope') 609 | .mockImplementation(() => scope); 610 | 611 | // when 612 | await request(app.getHttpServer()).get('/hi').send().expect(200); 613 | await request(app.getHttpServer()).get('/hello').send().expect(200); 614 | 615 | // then 616 | expect( 617 | Reflect.getMetadata( 618 | Constants.SPAN_METADATA_ACTIVE, 619 | HelloController.prototype.hi, 620 | ), 621 | ).toBeUndefined(); 622 | expect( 623 | Reflect.getMetadata( 624 | Constants.SPAN_METADATA_ACTIVE, 625 | HelloController.prototype.hello, 626 | ), 627 | ).toBeUndefined(); 628 | expect( 629 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hi), 630 | ).toBe('/hi'); 631 | expect( 632 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hello), 633 | ).toBe('/hello'); 634 | 635 | expect(tracer.startSpan).not.toHaveBeenCalled(); 636 | expect(tracer.scope().active).not.toHaveBeenCalled(); 637 | expect(tracer.scope().activate).not.toHaveBeenCalled(); 638 | expect(mockSpan.finish).not.toHaveBeenCalled(); 639 | 640 | startSpanSpy.mockClear(); 641 | scopeSpy.mockClear(); 642 | }); 643 | 644 | it('should exclude methods in controller', async () => { 645 | // given 646 | @Controller() 647 | @Span() 648 | class HelloController { 649 | @Get('/hi') 650 | hi() { 651 | return 0; 652 | } 653 | @NoSpan() 654 | @Get('/hello') 655 | hello() { 656 | return 1; 657 | } 658 | } 659 | 660 | const module: TestingModule = await Test.createTestingModule({ 661 | imports: [DatadogTraceModule.forRoot()], 662 | controllers: [HelloController], 663 | }).compile(); 664 | const app = module.createNestApplication(); 665 | await app.init(); 666 | 667 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 668 | const startSpanSpy = jest 669 | .spyOn(tracer, 'startSpan') 670 | .mockReturnValue(mockSpan); 671 | const scope = { 672 | active: jest.fn(() => null) as any, 673 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 674 | return fn(); 675 | }) as any, 676 | } as Scope; 677 | const scopeSpy = jest 678 | .spyOn(tracer, 'scope') 679 | .mockImplementation(() => scope); 680 | 681 | // when 682 | await request(app.getHttpServer()).get('/hi').send().expect(200); 683 | await request(app.getHttpServer()).get('/hello').send().expect(200); 684 | 685 | // then 686 | expect( 687 | Reflect.getMetadata( 688 | Constants.SPAN_METADATA_ACTIVE, 689 | HelloController.prototype.hi, 690 | ), 691 | ).toBe(1); 692 | expect( 693 | Reflect.getMetadata( 694 | Constants.SPAN_METADATA_ACTIVE, 695 | HelloController.prototype.hello, 696 | ), 697 | ).toBeUndefined(); 698 | expect( 699 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hi), 700 | ).toBe('/hi'); 701 | expect( 702 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hello), 703 | ).toBe('/hello'); 704 | 705 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hi', { 706 | childOf: null, 707 | }); 708 | expect(tracer.startSpan).not.toHaveBeenCalledWith('HelloController.hello', { 709 | childOf: null, 710 | }); 711 | expect(tracer.scope().active).toHaveBeenCalledTimes(1); 712 | expect(tracer.scope().activate).toHaveBeenCalledTimes(1); 713 | expect(mockSpan.finish).toHaveBeenCalledTimes(1); 714 | 715 | startSpanSpy.mockClear(); 716 | scopeSpy.mockClear(); 717 | }); 718 | 719 | it('should be usable with other annotations', async () => { 720 | // eslint-disable-next-line prettier/prettier 721 | const pipe = new (function transform() { })(); 722 | 723 | // given 724 | @Controller() 725 | @Span() 726 | class HelloController { 727 | @EventPattern('pattern1', Transport.KAFKA) 728 | @UsePipes(pipe, pipe) 729 | hi() { 730 | return 0; 731 | } 732 | @EventPattern('pattern2', Transport.KAFKA) 733 | @UsePipes(pipe, pipe) 734 | hello() { 735 | return 1; 736 | } 737 | } 738 | 739 | await Test.createTestingModule({ 740 | imports: [DatadogTraceModule.forRoot()], 741 | controllers: [HelloController], 742 | }).compile(); 743 | 744 | // then 745 | expect( 746 | Reflect.getMetadata( 747 | Constants.SPAN_METADATA_ACTIVE, 748 | HelloController.prototype.hi, 749 | ), 750 | ).toBe(1); 751 | expect( 752 | Reflect.getMetadata(PATTERN_METADATA, HelloController.prototype.hi), 753 | ).toEqual(['pattern1']); 754 | expect( 755 | Reflect.getMetadata( 756 | PATTERN_HANDLER_METADATA, 757 | HelloController.prototype.hi, 758 | ), 759 | ).toBe(PatternHandler.EVENT); 760 | expect( 761 | Reflect.getMetadata(TRANSPORT_METADATA, HelloController.prototype.hi), 762 | ).toBe(Transport.KAFKA); 763 | expect( 764 | Reflect.getMetadata(PIPES_METADATA, HelloController.prototype.hi), 765 | ).toEqual([pipe, pipe]); 766 | 767 | expect( 768 | Reflect.getMetadata( 769 | Constants.SPAN_METADATA_ACTIVE, 770 | HelloController.prototype.hello, 771 | ), 772 | ).toBe(1); 773 | expect( 774 | Reflect.getMetadata(PATTERN_METADATA, HelloController.prototype.hello), 775 | ).toEqual(['pattern2']); 776 | expect( 777 | Reflect.getMetadata( 778 | PATTERN_HANDLER_METADATA, 779 | HelloController.prototype.hello, 780 | ), 781 | ).toBe(PatternHandler.EVENT); 782 | expect( 783 | Reflect.getMetadata(TRANSPORT_METADATA, HelloController.prototype.hello), 784 | ).toBe(Transport.KAFKA); 785 | expect( 786 | Reflect.getMetadata(PIPES_METADATA, HelloController.prototype.hello), 787 | ).toEqual([pipe, pipe]); 788 | }); 789 | 790 | it('should work with all methods in controller if options.controllers is enabled', async () => { 791 | // given 792 | @Controller() 793 | class HelloController { 794 | @Get('/hi') 795 | hi() { 796 | return 0; 797 | } 798 | @Get('/hello') 799 | hello() { 800 | return 1; 801 | } 802 | } 803 | 804 | @Controller('/foo') 805 | class WorldController { 806 | @Get('/bar') 807 | bar() { 808 | return 'bar'; 809 | } 810 | } 811 | 812 | const module: TestingModule = await Test.createTestingModule({ 813 | imports: [DatadogTraceModule.forRoot({ controllers: true })], 814 | controllers: [HelloController, WorldController], 815 | }).compile(); 816 | const app = module.createNestApplication(); 817 | await app.init(); 818 | 819 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 820 | const startSpanSpy = jest 821 | .spyOn(tracer, 'startSpan') 822 | .mockReturnValue(mockSpan); 823 | const scope = { 824 | active: jest.fn(() => null) as any, 825 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 826 | return fn(); 827 | }) as any, 828 | } as Scope; 829 | const scopeSpy = jest 830 | .spyOn(tracer, 'scope') 831 | .mockImplementation(() => scope); 832 | 833 | // when 834 | await request(app.getHttpServer()).get('/hi').send().expect(200); 835 | await request(app.getHttpServer()).get('/hello').send().expect(200); 836 | await request(app.getHttpServer()).get('/foo/bar').send().expect(200); 837 | 838 | // then 839 | expect( 840 | Reflect.getMetadata( 841 | Constants.SPAN_METADATA_ACTIVE, 842 | HelloController.prototype.hi, 843 | ), 844 | ).toBe(1); 845 | expect( 846 | Reflect.getMetadata( 847 | Constants.SPAN_METADATA_ACTIVE, 848 | HelloController.prototype.hello, 849 | ), 850 | ).toBe(1); 851 | expect( 852 | Reflect.getMetadata( 853 | Constants.SPAN_METADATA_ACTIVE, 854 | WorldController.prototype.bar, 855 | ), 856 | ).toBe(1); 857 | expect( 858 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hi), 859 | ).toBe('/hi'); 860 | expect( 861 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hello), 862 | ).toBe('/hello'); 863 | expect( 864 | Reflect.getMetadata(PATH_METADATA, WorldController.prototype.bar), 865 | ).toBe('/bar'); 866 | 867 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hi', { 868 | childOf: null, 869 | }); 870 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hello', { 871 | childOf: null, 872 | }); 873 | expect(tracer.startSpan).toHaveBeenCalledWith('WorldController.bar', { 874 | childOf: null, 875 | }); 876 | expect(tracer.scope().active).toHaveBeenCalledTimes(3); 877 | expect(tracer.scope().activate).toHaveBeenCalledTimes(3); 878 | expect(mockSpan.finish).toHaveBeenCalledTimes(3); 879 | 880 | startSpanSpy.mockClear(); 881 | scopeSpy.mockClear(); 882 | }); 883 | 884 | it('should exclude some methods in controller if options.controllers is enabled', async () => { 885 | // given 886 | @Controller() 887 | class HelloController { 888 | @Get('/hi') 889 | hi() { 890 | return 0; 891 | } 892 | @NoSpan() 893 | @Get('/hello') 894 | hello() { 895 | return 1; 896 | } 897 | } 898 | 899 | const module: TestingModule = await Test.createTestingModule({ 900 | imports: [DatadogTraceModule.forRoot({ controllers: true })], 901 | controllers: [HelloController], 902 | }).compile(); 903 | const app = module.createNestApplication(); 904 | await app.init(); 905 | 906 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 907 | const startSpanSpy = jest 908 | .spyOn(tracer, 'startSpan') 909 | .mockReturnValue(mockSpan); 910 | const scope = { 911 | active: jest.fn(() => null) as any, 912 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 913 | return fn(); 914 | }) as any, 915 | } as Scope; 916 | const scopeSpy = jest 917 | .spyOn(tracer, 'scope') 918 | .mockImplementation(() => scope); 919 | 920 | // when 921 | await request(app.getHttpServer()).get('/hi').send().expect(200); 922 | await request(app.getHttpServer()).get('/hello').send().expect(200); 923 | 924 | // then 925 | expect( 926 | Reflect.getMetadata( 927 | Constants.SPAN_METADATA_ACTIVE, 928 | HelloController.prototype.hi, 929 | ), 930 | ).toBe(1); 931 | expect( 932 | Reflect.getMetadata( 933 | Constants.SPAN_METADATA_ACTIVE, 934 | HelloController.prototype.hello, 935 | ), 936 | ).toBeUndefined(); 937 | expect( 938 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hi), 939 | ).toBe('/hi'); 940 | expect( 941 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hello), 942 | ).toBe('/hello'); 943 | 944 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hi', { 945 | childOf: null, 946 | }); 947 | expect(tracer.startSpan).not.toHaveBeenCalledWith('HelloController.hello', { 948 | childOf: null, 949 | }); 950 | expect(tracer.scope().active).toHaveBeenCalledTimes(1); 951 | expect(tracer.scope().activate).toHaveBeenCalledTimes(1); 952 | expect(mockSpan.finish).toHaveBeenCalledTimes(1); 953 | 954 | startSpanSpy.mockClear(); 955 | scopeSpy.mockClear(); 956 | }); 957 | 958 | it('should exclude controller if included in options.excludeControllers', async () => { 959 | // given 960 | @Controller() 961 | class HelloController { 962 | @Get('/hi') 963 | hi() { 964 | return 0; 965 | } 966 | @Get('/hello') 967 | hello() { 968 | return 1; 969 | } 970 | } 971 | 972 | @Controller('/foo') 973 | class WorldController { 974 | @Get('/bar') 975 | bar() { 976 | return 'bar'; 977 | } 978 | } 979 | 980 | const module: TestingModule = await Test.createTestingModule({ 981 | imports: [ 982 | DatadogTraceModule.forRoot({ 983 | controllers: true, 984 | excludeControllers: ['WorldController'], 985 | }), 986 | ], 987 | controllers: [HelloController, WorldController], 988 | }).compile(); 989 | const app = module.createNestApplication(); 990 | await app.init(); 991 | 992 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 993 | const startSpanSpy = jest 994 | .spyOn(tracer, 'startSpan') 995 | .mockReturnValue(mockSpan); 996 | const scope = { 997 | active: jest.fn(() => null) as any, 998 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 999 | return fn(); 1000 | }) as any, 1001 | } as Scope; 1002 | const scopeSpy = jest 1003 | .spyOn(tracer, 'scope') 1004 | .mockImplementation(() => scope); 1005 | 1006 | // when 1007 | await request(app.getHttpServer()).get('/hi').send().expect(200); 1008 | await request(app.getHttpServer()).get('/hello').send().expect(200); 1009 | await request(app.getHttpServer()).get('/foo/bar').send().expect(200); 1010 | 1011 | // then 1012 | expect( 1013 | Reflect.getMetadata( 1014 | Constants.SPAN_METADATA_ACTIVE, 1015 | HelloController.prototype.hi, 1016 | ), 1017 | ).toBe(1); 1018 | expect( 1019 | Reflect.getMetadata( 1020 | Constants.SPAN_METADATA_ACTIVE, 1021 | HelloController.prototype.hello, 1022 | ), 1023 | ).toBe(1); 1024 | expect( 1025 | Reflect.getMetadata( 1026 | Constants.SPAN_METADATA_ACTIVE, 1027 | WorldController.prototype.bar, 1028 | ), 1029 | ).toBeUndefined(); 1030 | expect( 1031 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hi), 1032 | ).toBe('/hi'); 1033 | expect( 1034 | Reflect.getMetadata(PATH_METADATA, HelloController.prototype.hello), 1035 | ).toBe('/hello'); 1036 | expect( 1037 | Reflect.getMetadata(PATH_METADATA, WorldController.prototype.bar), 1038 | ).toBe('/bar'); 1039 | 1040 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hi', { 1041 | childOf: null, 1042 | }); 1043 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloController.hello', { 1044 | childOf: null, 1045 | }); 1046 | expect(tracer.startSpan).not.toHaveBeenCalledWith('WorldController.bar', { 1047 | childOf: null, 1048 | }); 1049 | expect(tracer.scope().active).toHaveBeenCalledTimes(2); 1050 | expect(tracer.scope().activate).toHaveBeenCalledTimes(2); 1051 | expect(mockSpan.finish).toHaveBeenCalledTimes(2); 1052 | 1053 | startSpanSpy.mockClear(); 1054 | scopeSpy.mockClear(); 1055 | }); 1056 | 1057 | it('should work with all methods in provider if options.providers is enabled', async () => { 1058 | // given 1059 | @Injectable() 1060 | class HelloService { 1061 | hi() { 1062 | return 0; 1063 | } 1064 | hello() { 1065 | return 1; 1066 | } 1067 | } 1068 | 1069 | @Injectable() 1070 | class WorldService { 1071 | foo() { 1072 | return 2; 1073 | } 1074 | } 1075 | 1076 | const module: TestingModule = await Test.createTestingModule({ 1077 | imports: [DatadogTraceModule.forRoot({ providers: true })], 1078 | providers: [HelloService, WorldService], 1079 | }).compile(); 1080 | 1081 | const helloService = module.get(HelloService); 1082 | const worldService = module.get(WorldService); 1083 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 1084 | const startSpanSpy = jest 1085 | .spyOn(tracer, 'startSpan') 1086 | .mockReturnValue(mockSpan); 1087 | const scope = { 1088 | active: jest.fn(() => null) as any, 1089 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 1090 | return fn(); 1091 | }) as any, 1092 | } as Scope; 1093 | const scopeSpy = jest 1094 | .spyOn(tracer, 'scope') 1095 | .mockImplementation(() => scope); 1096 | 1097 | // when 1098 | const result1 = helloService.hi(); 1099 | const result2 = helloService.hello(); 1100 | const result3 = worldService.foo(); 1101 | 1102 | // then 1103 | expect(result1).toBe(0); 1104 | expect(result2).toBe(1); 1105 | expect(result3).toBe(2); 1106 | expect( 1107 | Reflect.getMetadata( 1108 | Constants.SPAN_METADATA_ACTIVE, 1109 | HelloService.prototype.hi, 1110 | ), 1111 | ).toBe(1); 1112 | expect( 1113 | Reflect.getMetadata( 1114 | Constants.SPAN_METADATA_ACTIVE, 1115 | HelloService.prototype.hello, 1116 | ), 1117 | ).toBe(1); 1118 | expect( 1119 | Reflect.getMetadata( 1120 | Constants.SPAN_METADATA_ACTIVE, 1121 | WorldService.prototype.foo, 1122 | ), 1123 | ).toBe(1); 1124 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hi', { 1125 | childOf: null, 1126 | }); 1127 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hello', { 1128 | childOf: null, 1129 | }); 1130 | expect(tracer.startSpan).toHaveBeenCalledWith('WorldService.foo', { 1131 | childOf: null, 1132 | }); 1133 | expect(tracer.scope().active).toHaveBeenCalledTimes(3); 1134 | expect(tracer.scope().activate).toHaveBeenCalledTimes(3); 1135 | expect(mockSpan.finish).toHaveBeenCalledTimes(3); 1136 | 1137 | startSpanSpy.mockClear(); 1138 | scopeSpy.mockClear(); 1139 | }); 1140 | 1141 | it('should exclude some methods in provider if options.providers is enabled', async () => { 1142 | // given 1143 | @Injectable() 1144 | class HelloService { 1145 | hi() { 1146 | return 0; 1147 | } 1148 | @NoSpan() 1149 | hello() { 1150 | return 1; 1151 | } 1152 | } 1153 | 1154 | const module: TestingModule = await Test.createTestingModule({ 1155 | imports: [DatadogTraceModule.forRoot({ providers: true })], 1156 | providers: [HelloService], 1157 | }).compile(); 1158 | 1159 | const helloService = module.get(HelloService); 1160 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 1161 | const startSpanSpy = jest 1162 | .spyOn(tracer, 'startSpan') 1163 | .mockReturnValue(mockSpan); 1164 | const scope = { 1165 | active: jest.fn(() => null) as any, 1166 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 1167 | return fn(); 1168 | }) as any, 1169 | } as Scope; 1170 | const scopeSpy = jest 1171 | .spyOn(tracer, 'scope') 1172 | .mockImplementation(() => scope); 1173 | 1174 | // when 1175 | const result1 = helloService.hi(); 1176 | const result2 = helloService.hello(); 1177 | 1178 | // then 1179 | expect(result1).toBe(0); 1180 | expect(result2).toBe(1); 1181 | expect( 1182 | Reflect.getMetadata( 1183 | Constants.SPAN_METADATA_ACTIVE, 1184 | HelloService.prototype.hi, 1185 | ), 1186 | ).toBe(1); 1187 | expect( 1188 | Reflect.getMetadata( 1189 | Constants.SPAN_METADATA_ACTIVE, 1190 | HelloService.prototype.hello, 1191 | ), 1192 | ).toBeUndefined(); 1193 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hi', { 1194 | childOf: null, 1195 | }); 1196 | expect(tracer.startSpan).not.toHaveBeenCalledWith('HelloService.hello', { 1197 | childOf: null, 1198 | }); 1199 | expect(tracer.scope().active).toHaveBeenCalledTimes(1); 1200 | expect(tracer.scope().activate).toHaveBeenCalledTimes(1); 1201 | expect(mockSpan.finish).toHaveBeenCalledTimes(1); 1202 | 1203 | startSpanSpy.mockClear(); 1204 | scopeSpy.mockClear(); 1205 | }); 1206 | 1207 | it('should exclude provider if included in options.excludeProviders', async () => { 1208 | // given 1209 | @Injectable() 1210 | class HelloService { 1211 | hi() { 1212 | return 0; 1213 | } 1214 | hello() { 1215 | return 1; 1216 | } 1217 | } 1218 | 1219 | @Injectable() 1220 | class WorldService { 1221 | foo() { 1222 | return 2; 1223 | } 1224 | } 1225 | 1226 | const module: TestingModule = await Test.createTestingModule({ 1227 | imports: [ 1228 | DatadogTraceModule.forRoot({ 1229 | providers: true, 1230 | excludeProviders: ['WorldService'], 1231 | }), 1232 | ], 1233 | providers: [HelloService, WorldService], 1234 | }).compile(); 1235 | 1236 | const helloService = module.get(HelloService); 1237 | const worldService = module.get(WorldService); 1238 | const mockSpan = { finish: jest.fn() as any } as TraceSpan; 1239 | const startSpanSpy = jest 1240 | .spyOn(tracer, 'startSpan') 1241 | .mockReturnValue(mockSpan); 1242 | const scope = { 1243 | active: jest.fn(() => null) as any, 1244 | activate: jest.fn((span: TraceSpan, fn: (...args: any[]) => any): any => { 1245 | return fn(); 1246 | }) as any, 1247 | } as Scope; 1248 | const scopeSpy = jest 1249 | .spyOn(tracer, 'scope') 1250 | .mockImplementation(() => scope); 1251 | 1252 | // when 1253 | const result1 = helloService.hi(); 1254 | const result2 = helloService.hello(); 1255 | const result3 = worldService.foo(); 1256 | 1257 | // then 1258 | expect(result1).toBe(0); 1259 | expect(result2).toBe(1); 1260 | expect(result3).toBe(2); 1261 | expect( 1262 | Reflect.getMetadata( 1263 | Constants.SPAN_METADATA_ACTIVE, 1264 | HelloService.prototype.hi, 1265 | ), 1266 | ).toBe(1); 1267 | expect( 1268 | Reflect.getMetadata( 1269 | Constants.SPAN_METADATA_ACTIVE, 1270 | HelloService.prototype.hello, 1271 | ), 1272 | ).toBe(1); 1273 | expect( 1274 | Reflect.getMetadata( 1275 | Constants.SPAN_METADATA_ACTIVE, 1276 | WorldService.prototype.foo, 1277 | ), 1278 | ).toBeUndefined(); 1279 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hi', { 1280 | childOf: null, 1281 | }); 1282 | expect(tracer.startSpan).toHaveBeenCalledWith('HelloService.hello', { 1283 | childOf: null, 1284 | }); 1285 | expect(tracer.startSpan).not.toHaveBeenCalledWith('WorldService.foo', { 1286 | childOf: null, 1287 | }); 1288 | expect(tracer.scope().active).toHaveBeenCalledTimes(2); 1289 | expect(tracer.scope().activate).toHaveBeenCalledTimes(2); 1290 | expect(mockSpan.finish).toHaveBeenCalledTimes(2); 1291 | 1292 | startSpanSpy.mockClear(); 1293 | scopeSpy.mockClear(); 1294 | }); 1295 | }); 1296 | --------------------------------------------------------------------------------