Complete Sentry integration (#1546)

This commit is contained in:
Charles Bochet
2023-09-11 15:07:30 -07:00
committed by GitHub
parent 35bcef5090
commit 7621854d4b
13 changed files with 164 additions and 67 deletions

View File

@ -22,5 +22,6 @@ 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
# LOGGER_DRIVER=console
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx # SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
# LOGGER_DRIVER=console # LOG_LEVEL=error,warn

View File

@ -75,8 +75,7 @@ export class UserResolver {
select, select,
}); });
assert(user, 'User not found'); assert(user, 'User not found');
throw new Error('test2');
return user;
} }
@UseFilters(ExceptionFilter) @UseFilters(ExceptionFilter)

View File

@ -0,0 +1,58 @@
import { plainToClass } from 'class-transformer';
import { CastToLogLevelArray } from 'src/integrations/environment/decorators/cast-to-log-level-array.decorator';
class TestClass {
@CastToLogLevelArray()
logLevels?: any;
}
describe('CastToLogLevelArray Decorator', () => {
it('should cast "log" to ["log"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'log' });
expect(transformedClass.logLevels).toStrictEqual(['log']);
});
it('should cast "error" to ["error"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'error' });
expect(transformedClass.logLevels).toStrictEqual(['error']);
});
it('should cast "warn" to ["warn"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'warn' });
expect(transformedClass.logLevels).toStrictEqual(['warn']);
});
it('should cast "debug" to ["debug"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'debug' });
expect(transformedClass.logLevels).toStrictEqual(['debug']);
});
it('should cast "verbose" to ["verbose"]', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'verbose' });
expect(transformedClass.logLevels).toStrictEqual(['verbose']);
});
it('should cast "verbose,error,warn" to ["verbose", "error", "warn"]', () => {
const transformedClass = plainToClass(TestClass, {
logLevels: 'verbose,error,warn',
});
expect(transformedClass.logLevels).toStrictEqual([
'verbose',
'error',
'warn',
]);
});
it('should cast "toto" to undefined', () => {
const transformedClass = plainToClass(TestClass, { logLevels: 'toto' });
expect(transformedClass.logLevels).toBeUndefined();
});
it('should cast "verbose,error,toto" to undefined', () => {
const transformedClass = plainToClass(TestClass, {
logLevels: 'verbose,error,toto',
});
expect(transformedClass.logLevels).toBeUndefined();
});
});

View File

@ -0,0 +1,20 @@
import { Transform } from 'class-transformer';
export function CastToLogLevelArray() {
return Transform(({ value }: { value: string }) => toLogLevelArray(value));
}
const toLogLevelArray = (value: any) => {
if (typeof value === 'string') {
const rawLogLevels = value.split(',').map((level) => level.trim());
const isInvalid = rawLogLevels.some(
(level) => !['log', 'error', 'warn', 'debug', 'verbose'].includes(level),
);
if (!isInvalid) {
return rawLogLevels;
}
}
return undefined;
};

View File

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Injectable } from '@nestjs/common'; import { Injectable, LogLevel } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; 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'; import { LoggerDriver } from './interfaces/logger.interface';
@Injectable() @Injectable()
export class EnvironmentService { export class EnvironmentService {
@ -122,13 +122,23 @@ export class EnvironmentService {
return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY'); return this.configService.get<string>('SUPPORT_FRONT_HMAC_KEY');
} }
getLoggerDriver(): string {
return (
this.configService.get<string>('LOGGER_DRIVER') ?? LoggerDriver.Console
);
}
getLogLevels(): LogLevel[] {
return (
this.configService.get<LogLevel[]>('LOG_LEVELS') ?? [
'log',
'error',
'warn',
]
);
}
getSentryDSN(): string | undefined { getSentryDSN(): string | undefined {
return this.configService.get<string>('SENTRY_DSN'); return this.configService.get<string>('SENTRY_DSN');
} }
getLoggerDriver(): string | undefined {
return (
this.configService.get<string>('LOGGER_DRIVER') ?? LoggerType.Console
);
}
} }

View File

@ -1,3 +1,5 @@
import { LogLevel } from '@nestjs/common';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { import {
IsEnum, IsEnum,
@ -19,6 +21,8 @@ import { IsAWSRegion } from './decorators/is-aws-region.decorator';
import { CastToBoolean } from './decorators/cast-to-boolean.decorator'; import { CastToBoolean } from './decorators/cast-to-boolean.decorator';
import { SupportDriver } from './interfaces/support.interface'; import { SupportDriver } from './interfaces/support.interface';
import { CastToPositiveNumber } from './decorators/cast-to-positive-number.decorator'; import { CastToPositiveNumber } from './decorators/cast-to-positive-number.decorator';
import { LoggerDriver } from './interfaces/logger.interface';
import { CastToLogLevelArray } from './decorators/cast-to-log-level-array.decorator';
export class EnvironmentVariables { export class EnvironmentVariables {
// Misc // Misc
@ -125,6 +129,18 @@ export class EnvironmentVariables {
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front) @ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
@IsString() @IsString()
SUPPORT_FRONT_HMAC_KEY?: string; SUPPORT_FRONT_HMAC_KEY?: string;
@IsEnum(LoggerDriver)
@IsOptional()
LOGGER_DRIVER?: LoggerDriver;
@CastToLogLevelArray()
@IsOptional()
LOG_LEVELS?: LogLevel[];
@ValidateIf((env) => env.LOGGER_DRIVER === LoggerDriver.Sentry)
@IsString()
SENTRY_DSN?: string;
} }
export function validate(config: Record<string, unknown>) { export function validate(config: Record<string, unknown>) {

View File

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

View File

@ -8,8 +8,8 @@ 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 { LoggerModule } from './logger/logger.module';
import { LoggerType } from './environment/interfaces/logger.interface';
import { LoggerModuleOptions } from './logger/interfaces'; import { LoggerModuleOptions } from './logger/interfaces';
import { LoggerDriver } from './environment/interfaces/logger.interface';
/** /**
* FileStorage Module factory * FileStorage Module factory
@ -63,15 +63,15 @@ const loggerModuleFactory = async (
): Promise<LoggerModuleOptions> => { ): Promise<LoggerModuleOptions> => {
const type = environmentService.getLoggerDriver(); const type = environmentService.getLoggerDriver();
switch (type) { switch (type) {
case LoggerType.Console: { case LoggerDriver.Console: {
return { return {
type: LoggerType.Console, type: LoggerDriver.Console,
options: null, options: null,
}; };
} }
case LoggerType.Sentry: { case LoggerDriver.Sentry: {
return { return {
type: LoggerType.Sentry, type: LoggerDriver.Sentry,
options: { options: {
sentryDNS: environmentService.getSentryDSN() ?? '', sentryDNS: environmentService.getSentryDSN() ?? '',
}, },

View File

@ -15,53 +15,39 @@ export class SentryDriver implements LoggerService {
}); });
} }
log(message: any, category: string) { private logLevels = ['log', 'error', 'warning', 'debug', 'info'];
Sentry.addBreadcrumb({
message, setLogLevels(levels: string[]) {
level: 'log', this.logLevels = levels;
data: {
category,
},
});
} }
error(message: any, category: string) { log(message: any) {
Sentry.addBreadcrumb({ if (this.logLevels.includes('log')) {
message, Sentry.captureMessage(message, { level: 'log' });
level: 'error', }
data: {
category,
},
});
} }
warn(message: any, category: string) { error(message: any) {
Sentry.addBreadcrumb({ if (this.logLevels.includes('error')) {
message, Sentry.captureMessage(message, { level: 'error' });
level: 'error', }
data: {
category,
},
});
} }
debug?(message: any, category: string) { warn(message: any) {
Sentry.addBreadcrumb({ if (this.logLevels.includes('warn')) {
message, Sentry.captureMessage(message, { level: 'warning' });
level: 'debug', }
data: {
category,
},
});
} }
verbose?(message: any, category: string) { debug?(message: any) {
Sentry.addBreadcrumb({ if (this.logLevels.includes('debug')) {
message, Sentry.captureMessage(message, { level: 'debug' });
level: 'info', }
data: { }
category,
}, verbose?(message: any) {
}); if (this.logLevels.includes('verbose')) {
Sentry.captureMessage(message, { level: 'info' });
}
} }
} }

View File

@ -1,14 +1,14 @@
import { LoggerType } from 'src/integrations/environment/interfaces/logger.interface'; import { LoggerDriver } from 'src/integrations/environment/interfaces/logger.interface';
export interface SentryDriverFactoryOptions { export interface SentryDriverFactoryOptions {
type: LoggerType.Sentry; type: LoggerDriver.Sentry;
options: { options: {
sentryDNS: string; sentryDNS: string;
}; };
} }
export interface ConsoleDriverFactoryOptions { export interface ConsoleDriverFactoryOptions {
type: LoggerType.Console; type: LoggerDriver.Console;
options: null; options: null;
} }

View File

@ -1,6 +1,6 @@
import { DynamicModule, Global, ConsoleLogger } from '@nestjs/common'; import { DynamicModule, Global, ConsoleLogger } from '@nestjs/common';
import { LoggerType } from 'src/integrations/environment/interfaces/logger.interface'; import { LoggerDriver } from 'src/integrations/environment/interfaces/logger.interface';
import { LoggerService } from './logger.service'; import { LoggerService } from './logger.service';
import { LoggerModuleOptions } from './interfaces'; import { LoggerModuleOptions } from './interfaces';
@ -15,7 +15,7 @@ export class LoggerModule {
const provider = { const provider = {
provide: LOGGER_DRIVER, provide: LOGGER_DRIVER,
useValue: useValue:
options.type === LoggerType.Console options.type === LoggerDriver.Console
? new ConsoleLogger() ? new ConsoleLogger()
: new SentryDriver(options.options), : new SentryDriver(options.options),
}; };
@ -32,7 +32,7 @@ export class LoggerModule {
provide: LOGGER_DRIVER, provide: LOGGER_DRIVER,
useFactory: async (...args: any[]) => { useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args); const config = await options.useFactory(...args);
return config?.type === LoggerType.Console return config?.type === LoggerDriver.Console
? new ConsoleLogger() ? new ConsoleLogger()
: new SentryDriver(config.options); : new SentryDriver(config.options);
}, },

View File

@ -1,14 +1,15 @@
import { import {
Inject, Inject,
Injectable, Injectable,
LoggerService as ConsoleLoggerService, LogLevel,
LoggerService as LoggerServiceInterface,
} from '@nestjs/common'; } from '@nestjs/common';
import { LOGGER_DRIVER } from './logger.constants'; import { LOGGER_DRIVER } from './logger.constants';
@Injectable() @Injectable()
export class LoggerService implements ConsoleLoggerService { export class LoggerService implements LoggerServiceInterface {
constructor(@Inject(LOGGER_DRIVER) private driver: ConsoleLoggerService) {} constructor(@Inject(LOGGER_DRIVER) private driver: LoggerServiceInterface) {}
log(message: any, category: string, ...optionalParams: any[]) { log(message: any, category: string, ...optionalParams: any[]) {
this.driver.log.apply(this.driver, [message, category, ...optionalParams]); this.driver.log.apply(this.driver, [message, category, ...optionalParams]);
@ -41,4 +42,8 @@ export class LoggerService implements ConsoleLoggerService {
...optionalParams, ...optionalParams,
]); ]);
} }
setLogLevels(levels: LogLevel[]) {
this.driver.setLogLevels?.apply(this.driver, [levels]);
}
} }

View File

@ -9,6 +9,7 @@ import { AppModule } from './app.module';
import { settings } from './constants/settings'; import { settings } from './constants/settings';
import { LoggerService } from './integrations/logger/logger.service'; import { LoggerService } from './integrations/logger/logger.service';
import { EnvironmentService } from './integrations/environment/environment.service';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, { const app = await NestFactory.create(AppModule, {
@ -35,8 +36,9 @@ async function bootstrap() {
); );
const loggerService = app.get(LoggerService); const loggerService = app.get(LoggerService);
app.useLogger(loggerService); app.useLogger(loggerService);
app.useLogger(app.get(EnvironmentService).getLogLevels());
await app.listen(3000); await app.listen(app.get(EnvironmentService).getPort());
} }
bootstrap(); bootstrap();