From 35bcef5090b47cac4812146f88dd31f12777461b Mon Sep 17 00:00:00 2001 From: brendanlaschke Date: Mon, 11 Sep 2023 22:22:30 +0300 Subject: [PATCH] Add Sentry for Backend (#1403) * - added sentry * - renamed env var * - logger driver * - add breadcrumb and category * - fix driver --- server/.env.example | 2 + server/package.json | 2 + .../environment/environment.service.ts | 11 +++ .../interfaces/logger.interface.ts | 4 ++ .../src/integrations/integrations.module.ts | 36 ++++++++++ .../logger/drivers/sentry.driver.ts | 67 ++++++++++++++++++ .../integrations/logger/interfaces/index.ts | 1 + .../logger/interfaces/logger.interface.ts | 17 +++++ .../integrations/logger/logger.constants.ts | 1 + .../logger/logger.module-definition.ts | 25 +++++++ .../src/integrations/logger/logger.module.ts | 49 +++++++++++++ .../logger/logger.service.spec.ts | 26 +++++++ .../src/integrations/logger/logger.service.ts | 44 ++++++++++++ server/src/main.ts | 3 + server/yarn.lock | 68 +++++++++++++++++++ 15 files changed, 356 insertions(+) create mode 100644 server/src/integrations/environment/interfaces/logger.interface.ts create mode 100644 server/src/integrations/logger/drivers/sentry.driver.ts create mode 100644 server/src/integrations/logger/interfaces/index.ts create mode 100644 server/src/integrations/logger/interfaces/logger.interface.ts create mode 100644 server/src/integrations/logger/logger.constants.ts create mode 100644 server/src/integrations/logger/logger.module-definition.ts create mode 100644 server/src/integrations/logger/logger.module.ts create mode 100644 server/src/integrations/logger/logger.service.spec.ts create mode 100644 server/src/integrations/logger/logger.service.ts diff --git a/server/.env.example b/server/.env.example index 9a129d62e..597bf2489 100644 --- a/server/.env.example +++ b/server/.env.example @@ -22,3 +22,5 @@ SIGN_IN_PREFILLED=true # SUPPORT_DRIVER=front # SUPPORT_FRONT_HMAC_KEY=replace_me_with_front_chat_verification_secret # SUPPORT_FRONT_CHAT_ID=replace_me_with_front_chat_id +# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx +# LOGGER_DRIVER=console \ No newline at end of file diff --git a/server/package.json b/server/package.json index 4e92a83fd..0b8a73aaf 100644 --- a/server/package.json +++ b/server/package.json @@ -45,6 +45,8 @@ "@nestjs/terminus": "^9.2.2", "@paljs/plugins": "^5.3.3", "@prisma/client": "4.13.0", + "@sentry/node": "^7.66.0", + "@sentry/tracing": "^7.66.0", "@types/lodash.camelcase": "^4.3.7", "@types/lodash.merge": "^4.6.7", "add": "^2.0.6", diff --git a/server/src/integrations/environment/environment.service.ts b/server/src/integrations/environment/environment.service.ts index fe658d3bd..dda76254a 100644 --- a/server/src/integrations/environment/environment.service.ts +++ b/server/src/integrations/environment/environment.service.ts @@ -5,6 +5,7 @@ import { ConfigService } from '@nestjs/config'; import { AwsRegion } from './interfaces/aws-region.interface'; import { StorageType } from './interfaces/storage.interface'; import { SupportDriver } from './interfaces/support.interface'; +import { LoggerType } from './interfaces/logger.interface'; @Injectable() export class EnvironmentService { @@ -120,4 +121,14 @@ export class EnvironmentService { getSupportFrontHMACKey(): string | undefined { return this.configService.get('SUPPORT_FRONT_HMAC_KEY'); } + + getSentryDSN(): string | undefined { + return this.configService.get('SENTRY_DSN'); + } + + getLoggerDriver(): string | undefined { + return ( + this.configService.get('LOGGER_DRIVER') ?? LoggerType.Console + ); + } } diff --git a/server/src/integrations/environment/interfaces/logger.interface.ts b/server/src/integrations/environment/interfaces/logger.interface.ts new file mode 100644 index 000000000..d05a18a88 --- /dev/null +++ b/server/src/integrations/environment/interfaces/logger.interface.ts @@ -0,0 +1,4 @@ +export enum LoggerType { + Console = 'console', + Sentry = 'sentry', +} diff --git a/server/src/integrations/integrations.module.ts b/server/src/integrations/integrations.module.ts index 24330fbef..8a0231b82 100644 --- a/server/src/integrations/integrations.module.ts +++ b/server/src/integrations/integrations.module.ts @@ -7,6 +7,9 @@ import { EnvironmentService } from './environment/environment.service'; import { FileStorageModule } from './file-storage/file-storage.module'; import { FileStorageModuleOptions } from './file-storage/interfaces'; import { StorageType } from './environment/interfaces/storage.interface'; +import { LoggerModule } from './logger/logger.module'; +import { LoggerType } from './environment/interfaces/logger.interface'; +import { LoggerModuleOptions } from './logger/interfaces'; /** * FileStorage Module factory @@ -50,6 +53,35 @@ const fileStorageModuleFactory = async ( } }; +/** + * Logger Module factory + * @param environment + * @returns LoggerModuleOptions + */ +const loggerModuleFactory = async ( + environmentService: EnvironmentService, +): Promise => { + const type = environmentService.getLoggerDriver(); + switch (type) { + case LoggerType.Console: { + return { + type: LoggerType.Console, + options: null, + }; + } + case LoggerType.Sentry: { + return { + type: LoggerType.Sentry, + options: { + sentryDNS: environmentService.getSentryDSN() ?? '', + }, + }; + } + default: + throw new Error(`Invalid logger type (${type}), check your .env file`); + } +}; + @Module({ imports: [ EnvironmentModule.forRoot({}), @@ -57,6 +89,10 @@ const fileStorageModuleFactory = async ( useFactory: fileStorageModuleFactory, inject: [EnvironmentService], }), + LoggerModule.forRootAsync({ + useFactory: loggerModuleFactory, + inject: [EnvironmentService], + }), ], exports: [], providers: [], diff --git a/server/src/integrations/logger/drivers/sentry.driver.ts b/server/src/integrations/logger/drivers/sentry.driver.ts new file mode 100644 index 000000000..c27881b24 --- /dev/null +++ b/server/src/integrations/logger/drivers/sentry.driver.ts @@ -0,0 +1,67 @@ +import { LoggerService } from '@nestjs/common'; + +import * as Sentry from '@sentry/node'; + +export interface SentryDriverOptions { + sentryDNS: string; +} + +export class SentryDriver implements LoggerService { + constructor(options: SentryDriverOptions) { + Sentry.init({ + dsn: options.sentryDNS, + tracesSampleRate: 1.0, + profilesSampleRate: 1.0, + }); + } + + log(message: any, category: string) { + Sentry.addBreadcrumb({ + message, + level: 'log', + data: { + category, + }, + }); + } + + error(message: any, category: string) { + Sentry.addBreadcrumb({ + message, + level: 'error', + data: { + category, + }, + }); + } + + warn(message: any, category: string) { + Sentry.addBreadcrumb({ + message, + level: 'error', + data: { + category, + }, + }); + } + + debug?(message: any, category: string) { + Sentry.addBreadcrumb({ + message, + level: 'debug', + data: { + category, + }, + }); + } + + verbose?(message: any, category: string) { + Sentry.addBreadcrumb({ + message, + level: 'info', + data: { + category, + }, + }); + } +} diff --git a/server/src/integrations/logger/interfaces/index.ts b/server/src/integrations/logger/interfaces/index.ts new file mode 100644 index 000000000..2e75a7e00 --- /dev/null +++ b/server/src/integrations/logger/interfaces/index.ts @@ -0,0 +1 @@ +export * from './logger.interface'; diff --git a/server/src/integrations/logger/interfaces/logger.interface.ts b/server/src/integrations/logger/interfaces/logger.interface.ts new file mode 100644 index 000000000..9749cbf0f --- /dev/null +++ b/server/src/integrations/logger/interfaces/logger.interface.ts @@ -0,0 +1,17 @@ +import { LoggerType } from 'src/integrations/environment/interfaces/logger.interface'; + +export interface SentryDriverFactoryOptions { + type: LoggerType.Sentry; + options: { + sentryDNS: string; + }; +} + +export interface ConsoleDriverFactoryOptions { + type: LoggerType.Console; + options: null; +} + +export type LoggerModuleOptions = + | SentryDriverFactoryOptions + | ConsoleDriverFactoryOptions; diff --git a/server/src/integrations/logger/logger.constants.ts b/server/src/integrations/logger/logger.constants.ts new file mode 100644 index 000000000..6a1492293 --- /dev/null +++ b/server/src/integrations/logger/logger.constants.ts @@ -0,0 +1 @@ +export const LOGGER_DRIVER = Symbol('LOGGER_DRIVER'); diff --git a/server/src/integrations/logger/logger.module-definition.ts b/server/src/integrations/logger/logger.module-definition.ts new file mode 100644 index 000000000..7f1c1ee88 --- /dev/null +++ b/server/src/integrations/logger/logger.module-definition.ts @@ -0,0 +1,25 @@ +import { + ConfigurableModuleBuilder, + FactoryProvider, + ModuleMetadata, +} from '@nestjs/common'; + +import { LoggerModuleOptions } from './interfaces'; + +export const { + ConfigurableModuleClass, + MODULE_OPTIONS_TOKEN, + OPTIONS_TYPE, + ASYNC_OPTIONS_TYPE, +} = new ConfigurableModuleBuilder({ + moduleName: 'LoggerService', +}) + .setClassMethodName('forRoot') + .build(); + +export type LoggerModuleAsyncOptions = { + useFactory: ( + ...args: any[] + ) => LoggerModuleOptions | Promise; +} & Pick & + Pick; diff --git a/server/src/integrations/logger/logger.module.ts b/server/src/integrations/logger/logger.module.ts new file mode 100644 index 000000000..e3cf07b94 --- /dev/null +++ b/server/src/integrations/logger/logger.module.ts @@ -0,0 +1,49 @@ +import { DynamicModule, Global, ConsoleLogger } from '@nestjs/common'; + +import { LoggerType } from 'src/integrations/environment/interfaces/logger.interface'; + +import { LoggerService } from './logger.service'; +import { LoggerModuleOptions } from './interfaces'; +import { LOGGER_DRIVER } from './logger.constants'; +import { LoggerModuleAsyncOptions } from './logger.module-definition'; + +import { SentryDriver } from './drivers/sentry.driver'; + +@Global() +export class LoggerModule { + static forRoot(options: LoggerModuleOptions): DynamicModule { + const provider = { + provide: LOGGER_DRIVER, + useValue: + options.type === LoggerType.Console + ? new ConsoleLogger() + : new SentryDriver(options.options), + }; + + return { + module: LoggerModule, + providers: [LoggerService, provider], + exports: [LoggerService], + }; + } + + static forRootAsync(options: LoggerModuleAsyncOptions): DynamicModule { + const provider = { + provide: LOGGER_DRIVER, + useFactory: async (...args: any[]) => { + const config = await options.useFactory(...args); + return config?.type === LoggerType.Console + ? new ConsoleLogger() + : new SentryDriver(config.options); + }, + inject: options.inject || [], + }; + + return { + module: LoggerModule, + imports: options.imports || [], + providers: [LoggerService, provider], + exports: [LoggerService], + }; + } +} diff --git a/server/src/integrations/logger/logger.service.spec.ts b/server/src/integrations/logger/logger.service.spec.ts new file mode 100644 index 000000000..319e2a46d --- /dev/null +++ b/server/src/integrations/logger/logger.service.spec.ts @@ -0,0 +1,26 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { LoggerService } from './logger.service'; +import { LOGGER_DRIVER } from './logger.constants'; + +describe('LoggerService', () => { + let service: LoggerService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + LoggerService, + { + provide: LOGGER_DRIVER, + useValue: {}, + }, + ], + }).compile(); + + service = module.get(LoggerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/server/src/integrations/logger/logger.service.ts b/server/src/integrations/logger/logger.service.ts new file mode 100644 index 000000000..7602a7ce9 --- /dev/null +++ b/server/src/integrations/logger/logger.service.ts @@ -0,0 +1,44 @@ +import { + Inject, + Injectable, + LoggerService as ConsoleLoggerService, +} from '@nestjs/common'; + +import { LOGGER_DRIVER } from './logger.constants'; + +@Injectable() +export class LoggerService implements ConsoleLoggerService { + constructor(@Inject(LOGGER_DRIVER) private driver: ConsoleLoggerService) {} + + log(message: any, category: string, ...optionalParams: any[]) { + this.driver.log.apply(this.driver, [message, category, ...optionalParams]); + } + + error(message: any, category: string, ...optionalParams: any[]) { + this.driver.error.apply(this.driver, [ + message, + category, + ...optionalParams, + ]); + } + + warn(message: any, category: string, ...optionalParams: any[]) { + this.driver.warn.apply(this.driver, [message, category, ...optionalParams]); + } + + debug?(message: any, category: string, ...optionalParams: any[]) { + this.driver.debug?.apply(this.driver, [ + message, + category, + ...optionalParams, + ]); + } + + verbose?(message: any, category: string, ...optionalParams: any[]) { + this.driver.verbose?.apply(this.driver, [ + message, + category, + ...optionalParams, + ]); + } +} diff --git a/server/src/main.ts b/server/src/main.ts index 5174c526d..a92fba6e1 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -8,6 +8,7 @@ import bytes from 'bytes'; import { AppModule } from './app.module'; import { settings } from './constants/settings'; +import { LoggerService } from './integrations/logger/logger.service'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -32,6 +33,8 @@ async function bootstrap() { maxFiles: 10, }), ); + const loggerService = app.get(LoggerService); + app.useLogger(loggerService); await app.listen(3000); } diff --git a/server/yarn.lock b/server/yarn.lock index 079628a25..c139b8ded 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -2151,6 +2151,59 @@ resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@sentry-internal/tracing@7.66.0": + version "7.66.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.66.0.tgz#45ea607917d55a5bcaa3229341387ff6ed9b3a2b" + integrity sha512-3vCgC2hC3T45pn53yTDVcRpHoJTBxelDPPZVsipAbZnoOVPkj7n6dNfDhj3I3kwWCBPahPkXmE+R4xViR8VqJg== + dependencies: + "@sentry/core" "7.66.0" + "@sentry/types" "7.66.0" + "@sentry/utils" "7.66.0" + tslib "^2.4.1 || ^1.9.3" + +"@sentry/core@7.66.0": + version "7.66.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.66.0.tgz#8968f2a9e641d33e3750a8e24d1d39953680c4f2" + integrity sha512-WMAEPN86NeCJ1IT48Lqiz4MS5gdDjBwP4M63XP4msZn9aujSf2Qb6My5uT87AJr9zBtgk8MyJsuHr35F0P3q1w== + dependencies: + "@sentry/types" "7.66.0" + "@sentry/utils" "7.66.0" + tslib "^2.4.1 || ^1.9.3" + +"@sentry/node@^7.66.0": + version "7.66.0" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.66.0.tgz#d3e08471e1ecae28d3cd0ba3c18487ecb2449881" + integrity sha512-PxqIqLr4Sh5xcDfECiBQ4PuZ7v8yTgLhaRkruWrZPYxQrcJFPkwbFkw/IskzVnhT2VwXUmeWEIlRMQKBJ0t83A== + dependencies: + "@sentry-internal/tracing" "7.66.0" + "@sentry/core" "7.66.0" + "@sentry/types" "7.66.0" + "@sentry/utils" "7.66.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^2.4.1 || ^1.9.3" + +"@sentry/tracing@^7.66.0": + version "7.66.0" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.66.0.tgz#32e0e4c049abcbdbba793afdfe177f3d112242cf" + integrity sha512-9bnz2EcOwjeMZAuYJnrwcRrImu9c10p7A0iDB8b2HLcp7gpuCkJbJyGoC1xeKD7reVD0BPq3VIbeHSwCcQufoQ== + dependencies: + "@sentry-internal/tracing" "7.66.0" + +"@sentry/types@7.66.0": + version "7.66.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.66.0.tgz#4ec290cc6a3dd2024a61a0bffb468cedb409f7fb" + integrity sha512-uUMSoSiar6JhuD8p7ON/Ddp4JYvrVd2RpwXJRPH1A4H4Bd4DVt1mKJy1OLG6HdeQv39XyhB1lPZckKJg4tATPw== + +"@sentry/utils@7.66.0": + version "7.66.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.66.0.tgz#2e37c96610f26bc79ac064fca4222ea91fece68d" + integrity sha512-9GYUVgXjK66uXXcLXVMXVzlptqMtq1eJENCuDeezQiEFrNA71KkLDg00wESp+LL+bl3wpVTBApArpbF6UEG5hQ== + dependencies: + "@sentry/types" "7.66.0" + tslib "^2.4.1 || ^1.9.3" + "@sinclair/typebox@^0.24.1": version "0.24.51" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz" @@ -4317,6 +4370,11 @@ cookie@0.5.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz" @@ -6711,6 +6769,11 @@ lru-cache@^9.1.1: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz" integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ== +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + macos-release@^2.5.0: version "2.5.1" resolved "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz" @@ -8570,6 +8633,11 @@ tslib@^2.3.1, tslib@^2.5.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== +"tslib@^2.4.1 || ^1.9.3": + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"