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
15 changed files with 356 additions and 0 deletions

View File

@ -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<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 { 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<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({
imports: [
EnvironmentModule.forRoot({}),
@ -57,6 +89,10 @@ const fileStorageModuleFactory = async (
useFactory: fileStorageModuleFactory,
inject: [EnvironmentService],
}),
LoggerModule.forRootAsync({
useFactory: loggerModuleFactory,
inject: [EnvironmentService],
}),
],
exports: [],
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 { 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);
}