diff --git a/src/core/apm/apm.interceptor.ts b/src/core/apm/apm.interceptor.ts new file mode 100644 index 0000000..8085cce --- /dev/null +++ b/src/core/apm/apm.interceptor.ts @@ -0,0 +1,36 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + HttpException, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { ApmService } from './apm.service'; + +@Injectable() +export class ApmInterceptor implements NestInterceptor { + constructor(private readonly apmService: ApmService) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + const request = context.switchToHttp().getRequest(); + const user = request.headers['e-user']; + const rules = request.headers['e-rules']; + this.apmService.setCustomContext({ user, rules }); + + return next.handle().pipe( + catchError((error) => { + if (error instanceof HttpException) { + this.apmService.captureError(error.message); + } else { + this.apmService.captureError(error); + } + throw error; + }), + ); + } +} diff --git a/src/core/apm/apm.module.ts b/src/core/apm/apm.module.ts new file mode 100644 index 0000000..20a327c --- /dev/null +++ b/src/core/apm/apm.module.ts @@ -0,0 +1,22 @@ +import { DynamicModule } from '@nestjs/common'; +import { ApmService } from './apm.service'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { ApmInterceptor } from './apm.interceptor'; +// import { startAPM } from './start' + +export class ApmModule { + static register(): DynamicModule { + return { + module: ApmModule, + imports: [], + providers: [ + ApmService, + { + provide: APP_INTERCEPTOR, + useClass: ApmInterceptor, + }, + ], + exports: [ApmService], + }; + } +} diff --git a/src/core/apm/apm.service.ts b/src/core/apm/apm.service.ts new file mode 100644 index 0000000..4d47d7f --- /dev/null +++ b/src/core/apm/apm.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import * as APM from 'elastic-apm-node'; +import apm = require('elastic-apm-node'); + +@Injectable() +export class ApmService { + private readonly apm: apm.Agent; + + constructor() { + this.apm = APM; + } + + captureError(data: Error | string): void { + this.apm.captureError(data); + } + + startTransaction( + name?: string, + options?: apm.TransactionOptions, + ): apm.Transaction | null { + return this.apm.startTransaction(name, options); + } + + setTransactionName(name: string): void { + this.apm.setTransactionName(name); + } + + startSpan(name?: string, options?: apm.SpanOptions): apm.Span | null { + return this.apm.startSpan(name, options); + } + + setCustomContext(context: Record): void { + this.apm.setCustomContext(context); + } +} diff --git a/src/core/apm/index.ts b/src/core/apm/index.ts new file mode 100644 index 0000000..b09ff7e --- /dev/null +++ b/src/core/apm/index.ts @@ -0,0 +1,4 @@ +export * from './start'; +export * from './apm.module'; +export * from './apm.service'; +export * from './apm.interceptor'; diff --git a/src/core/apm/start.ts b/src/core/apm/start.ts new file mode 100644 index 0000000..80312fd --- /dev/null +++ b/src/core/apm/start.ts @@ -0,0 +1,27 @@ +import * as dotenv from 'dotenv'; +dotenv.config(); // + +import * as apmAgent from 'elastic-apm-node'; + +const options: apmAgent.AgentConfigOptions = { + active: process.env.ELASTIC_APM_ACTIVATE === 'true', +}; +if (process.env.ELASTIC_APM_SERVICE_NAME) { + options['serviceName'] = process.env.ELASTIC_APM_SERVICE_NAME; +} +if (process.env.ELASTIC_APM_SECRET_TOKEN) { + options['secretToken'] = process.env.ELASTIC_APM_SECRET_TOKEN; +} +if (process.env.ELASTIC_APM_API_KEY) { + options['apiKey'] = process.env.ELASTIC_APM_API_KEY; +} +if (process.env.ELASTIC_APM_SERVER_URL) { + options['serverUrl'] = process.env.ELASTIC_APM_SERVER_URL; +} +if (process.env.ELASTIC_APM_DISABLE_INSTRUMENTATIONS) { + options['disableInstrumentations'] = + process.env.ELASTIC_APM_DISABLE_INSTRUMENTATIONS.split(','); +} + +const apm: apmAgent.Agent = apmAgent.start(options); +export { apm };