Add Sentry for Backend (#1403)

* - added sentry

* - renamed env var

* - logger driver

* - add breadcrumb and category

* - fix driver
This commit is contained in:
brendanlaschke 2023-09-11 22:22:30 +03:00 committed by GitHub
parent 110d5eaa9d
commit 35bcef5090
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 356 additions and 0 deletions

View File

@ -22,3 +22,5 @@ SIGN_IN_PREFILLED=true
# SUPPORT_DRIVER=front # SUPPORT_DRIVER=front
# SUPPORT_FRONT_HMAC_KEY=replace_me_with_front_chat_verification_secret # SUPPORT_FRONT_HMAC_KEY=replace_me_with_front_chat_verification_secret
# SUPPORT_FRONT_CHAT_ID=replace_me_with_front_chat_id # SUPPORT_FRONT_CHAT_ID=replace_me_with_front_chat_id
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
# LOGGER_DRIVER=console

View File

@ -45,6 +45,8 @@
"@nestjs/terminus": "^9.2.2", "@nestjs/terminus": "^9.2.2",
"@paljs/plugins": "^5.3.3", "@paljs/plugins": "^5.3.3",
"@prisma/client": "4.13.0", "@prisma/client": "4.13.0",
"@sentry/node": "^7.66.0",
"@sentry/tracing": "^7.66.0",
"@types/lodash.camelcase": "^4.3.7", "@types/lodash.camelcase": "^4.3.7",
"@types/lodash.merge": "^4.6.7", "@types/lodash.merge": "^4.6.7",
"add": "^2.0.6", "add": "^2.0.6",

View File

@ -5,6 +5,7 @@ import { ConfigService } from '@nestjs/config';
import { AwsRegion } from './interfaces/aws-region.interface'; import { AwsRegion } from './interfaces/aws-region.interface';
import { StorageType } from './interfaces/storage.interface'; import { StorageType } from './interfaces/storage.interface';
import { SupportDriver } from './interfaces/support.interface'; import { SupportDriver } from './interfaces/support.interface';
import { LoggerType } from './interfaces/logger.interface';
@Injectable() @Injectable()
export class EnvironmentService { export class EnvironmentService {
@ -120,4 +121,14 @@ export class EnvironmentService {
getSupportFrontHMACKey(): string | undefined { getSupportFrontHMACKey(): string | undefined {
return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY'); return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY');
} }
getSentryDSN(): string | undefined {
return this.configService.get<string>('SENTRY_DSN');
}
getLoggerDriver(): string | undefined {
return (
this.configService.get<string>('LOGGER_DRIVER') ?? LoggerType.Console
);
}
} }

View File

@ -0,0 +1,4 @@
export enum LoggerType {
Console = 'console',
Sentry = 'sentry',
}

View File

@ -7,6 +7,9 @@ import { EnvironmentService } from './environment/environment.service';
import { FileStorageModule } from './file-storage/file-storage.module'; import { FileStorageModule } from './file-storage/file-storage.module';
import { FileStorageModuleOptions } from './file-storage/interfaces'; import { FileStorageModuleOptions } from './file-storage/interfaces';
import { StorageType } from './environment/interfaces/storage.interface'; 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 * FileStorage Module factory
@ -50,6 +53,35 @@ const fileStorageModuleFactory = async (
} }
}; };
/**
* Logger Module factory
* @param environment
* @returns LoggerModuleOptions
*/
const loggerModuleFactory = async (
environmentService: EnvironmentService,
): Promise<LoggerModuleOptions> => {
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({ @Module({
imports: [ imports: [
EnvironmentModule.forRoot({}), EnvironmentModule.forRoot({}),
@ -57,6 +89,10 @@ const fileStorageModuleFactory = async (
useFactory: fileStorageModuleFactory, useFactory: fileStorageModuleFactory,
inject: [EnvironmentService], inject: [EnvironmentService],
}), }),
LoggerModule.forRootAsync({
useFactory: loggerModuleFactory,
inject: [EnvironmentService],
}),
], ],
exports: [], exports: [],
providers: [], providers: [],

View File

@ -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,
},
});
}
}

View File

@ -0,0 +1 @@
export * from './logger.interface';

View File

@ -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;

View File

@ -0,0 +1 @@
export const LOGGER_DRIVER = Symbol('LOGGER_DRIVER');

View File

@ -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<LoggerModuleOptions>({
moduleName: 'LoggerService',
})
.setClassMethodName('forRoot')
.build();
export type LoggerModuleAsyncOptions = {
useFactory: (
...args: any[]
) => LoggerModuleOptions | Promise<LoggerModuleOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;

View File

@ -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],
};
}
}

View File

@ -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>(LoggerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -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,
]);
}
}

View File

@ -8,6 +8,7 @@ import bytes from 'bytes';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { settings } from './constants/settings'; import { settings } from './constants/settings';
import { LoggerService } from './integrations/logger/logger.service';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
@ -32,6 +33,8 @@ async function bootstrap() {
maxFiles: 10, maxFiles: 10,
}), }),
); );
const loggerService = app.get(LoggerService);
app.useLogger(loggerService);
await app.listen(3000); await app.listen(3000);
} }

View File

@ -2151,6 +2151,59 @@
resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz" resolved "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz"
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== 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": "@sinclair/typebox@^0.24.1":
version "0.24.51" version "0.24.51"
resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz" 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" resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 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: cookiejar@^2.1.4:
version "2.1.4" version "2.1.4"
resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz" 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" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz"
integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ== 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: macos-release@^2.5.0:
version "2.5.1" version "2.5.1"
resolved "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz" 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" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3"
integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== 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: tsutils@^3.21.0:
version "3.21.0" version "3.21.0"
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"