diff --git a/server/.env.example b/server/.env.example index 597bf2489f..5edf5a936a 100644 --- a/server/.env.example +++ b/server/.env.example @@ -22,5 +22,6 @@ 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 +# LOGGER_DRIVER=console # SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx -# LOGGER_DRIVER=console \ No newline at end of file +# LOG_LEVEL=error,warn \ No newline at end of file diff --git a/server/src/core/user/user.resolver.ts b/server/src/core/user/user.resolver.ts index c9c0b6063a..c585282b20 100644 --- a/server/src/core/user/user.resolver.ts +++ b/server/src/core/user/user.resolver.ts @@ -75,8 +75,7 @@ export class UserResolver { select, }); assert(user, 'User not found'); - - return user; + throw new Error('test2'); } @UseFilters(ExceptionFilter) diff --git a/server/src/integrations/environment/decorators/__tests__/cast-to-log-level-array.decorator.spec.ts b/server/src/integrations/environment/decorators/__tests__/cast-to-log-level-array.decorator.spec.ts new file mode 100644 index 0000000000..267688fe00 --- /dev/null +++ b/server/src/integrations/environment/decorators/__tests__/cast-to-log-level-array.decorator.spec.ts @@ -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(); + }); +}); diff --git a/server/src/integrations/environment/decorators/cast-to-log-level-array.decorator.ts b/server/src/integrations/environment/decorators/cast-to-log-level-array.decorator.ts new file mode 100644 index 0000000000..857d5536b2 --- /dev/null +++ b/server/src/integrations/environment/decorators/cast-to-log-level-array.decorator.ts @@ -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; +}; diff --git a/server/src/integrations/environment/environment.service.ts b/server/src/integrations/environment/environment.service.ts index dda76254af..90ea8042a3 100644 --- a/server/src/integrations/environment/environment.service.ts +++ b/server/src/integrations/environment/environment.service.ts @@ -1,11 +1,11 @@ /* 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 { AwsRegion } from './interfaces/aws-region.interface'; import { StorageType } from './interfaces/storage.interface'; import { SupportDriver } from './interfaces/support.interface'; -import { LoggerType } from './interfaces/logger.interface'; +import { LoggerDriver } from './interfaces/logger.interface'; @Injectable() export class EnvironmentService { @@ -122,13 +122,23 @@ export class EnvironmentService { return this.configService.get('SUPPORT_FRONT_HMAC_KEY'); } + getLoggerDriver(): string { + return ( + this.configService.get('LOGGER_DRIVER') ?? LoggerDriver.Console + ); + } + + getLogLevels(): LogLevel[] { + return ( + this.configService.get('LOG_LEVELS') ?? [ + 'log', + 'error', + 'warn', + ] + ); + } + 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/environment.validation.ts b/server/src/integrations/environment/environment.validation.ts index de70025bd9..30c630d015 100644 --- a/server/src/integrations/environment/environment.validation.ts +++ b/server/src/integrations/environment/environment.validation.ts @@ -1,3 +1,5 @@ +import { LogLevel } from '@nestjs/common'; + import { plainToClass } from 'class-transformer'; import { IsEnum, @@ -19,6 +21,8 @@ import { IsAWSRegion } from './decorators/is-aws-region.decorator'; import { CastToBoolean } from './decorators/cast-to-boolean.decorator'; import { SupportDriver } from './interfaces/support.interface'; 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 { // Misc @@ -125,6 +129,18 @@ export class EnvironmentVariables { @ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front) @IsString() 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) { diff --git a/server/src/integrations/environment/interfaces/logger.interface.ts b/server/src/integrations/environment/interfaces/logger.interface.ts index d05a18a88e..3692f062bc 100644 --- a/server/src/integrations/environment/interfaces/logger.interface.ts +++ b/server/src/integrations/environment/interfaces/logger.interface.ts @@ -1,4 +1,4 @@ -export enum LoggerType { +export enum LoggerDriver { Console = 'console', Sentry = 'sentry', } diff --git a/server/src/integrations/integrations.module.ts b/server/src/integrations/integrations.module.ts index 8a0231b82c..84206f1da2 100644 --- a/server/src/integrations/integrations.module.ts +++ b/server/src/integrations/integrations.module.ts @@ -8,8 +8,8 @@ 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'; +import { LoggerDriver } from './environment/interfaces/logger.interface'; /** * FileStorage Module factory @@ -63,15 +63,15 @@ const loggerModuleFactory = async ( ): Promise => { const type = environmentService.getLoggerDriver(); switch (type) { - case LoggerType.Console: { + case LoggerDriver.Console: { return { - type: LoggerType.Console, + type: LoggerDriver.Console, options: null, }; } - case LoggerType.Sentry: { + case LoggerDriver.Sentry: { return { - type: LoggerType.Sentry, + type: LoggerDriver.Sentry, options: { sentryDNS: environmentService.getSentryDSN() ?? '', }, diff --git a/server/src/integrations/logger/drivers/sentry.driver.ts b/server/src/integrations/logger/drivers/sentry.driver.ts index c27881b240..ddad9fdea0 100644 --- a/server/src/integrations/logger/drivers/sentry.driver.ts +++ b/server/src/integrations/logger/drivers/sentry.driver.ts @@ -15,53 +15,39 @@ export class SentryDriver implements LoggerService { }); } - log(message: any, category: string) { - Sentry.addBreadcrumb({ - message, - level: 'log', - data: { - category, - }, - }); + private logLevels = ['log', 'error', 'warning', 'debug', 'info']; + + setLogLevels(levels: string[]) { + this.logLevels = levels; } - error(message: any, category: string) { - Sentry.addBreadcrumb({ - message, - level: 'error', - data: { - category, - }, - }); + log(message: any) { + if (this.logLevels.includes('log')) { + Sentry.captureMessage(message, { level: 'log' }); + } } - warn(message: any, category: string) { - Sentry.addBreadcrumb({ - message, - level: 'error', - data: { - category, - }, - }); + error(message: any) { + if (this.logLevels.includes('error')) { + Sentry.captureMessage(message, { level: 'error' }); + } } - debug?(message: any, category: string) { - Sentry.addBreadcrumb({ - message, - level: 'debug', - data: { - category, - }, - }); + warn(message: any) { + if (this.logLevels.includes('warn')) { + Sentry.captureMessage(message, { level: 'warning' }); + } } - verbose?(message: any, category: string) { - Sentry.addBreadcrumb({ - message, - level: 'info', - data: { - category, - }, - }); + debug?(message: any) { + if (this.logLevels.includes('debug')) { + Sentry.captureMessage(message, { level: 'debug' }); + } + } + + verbose?(message: any) { + if (this.logLevels.includes('verbose')) { + Sentry.captureMessage(message, { level: 'info' }); + } } } diff --git a/server/src/integrations/logger/interfaces/logger.interface.ts b/server/src/integrations/logger/interfaces/logger.interface.ts index 9749cbf0f5..88985257d5 100644 --- a/server/src/integrations/logger/interfaces/logger.interface.ts +++ b/server/src/integrations/logger/interfaces/logger.interface.ts @@ -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 { - type: LoggerType.Sentry; + type: LoggerDriver.Sentry; options: { sentryDNS: string; }; } export interface ConsoleDriverFactoryOptions { - type: LoggerType.Console; + type: LoggerDriver.Console; options: null; } diff --git a/server/src/integrations/logger/logger.module.ts b/server/src/integrations/logger/logger.module.ts index e3cf07b94b..5d9ff00858 100644 --- a/server/src/integrations/logger/logger.module.ts +++ b/server/src/integrations/logger/logger.module.ts @@ -1,6 +1,6 @@ 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 { LoggerModuleOptions } from './interfaces'; @@ -15,7 +15,7 @@ export class LoggerModule { const provider = { provide: LOGGER_DRIVER, useValue: - options.type === LoggerType.Console + options.type === LoggerDriver.Console ? new ConsoleLogger() : new SentryDriver(options.options), }; @@ -32,7 +32,7 @@ export class LoggerModule { provide: LOGGER_DRIVER, useFactory: async (...args: any[]) => { const config = await options.useFactory(...args); - return config?.type === LoggerType.Console + return config?.type === LoggerDriver.Console ? new ConsoleLogger() : new SentryDriver(config.options); }, diff --git a/server/src/integrations/logger/logger.service.ts b/server/src/integrations/logger/logger.service.ts index 7602a7ce9d..882a53c17c 100644 --- a/server/src/integrations/logger/logger.service.ts +++ b/server/src/integrations/logger/logger.service.ts @@ -1,14 +1,15 @@ import { Inject, Injectable, - LoggerService as ConsoleLoggerService, + LogLevel, + LoggerService as LoggerServiceInterface, } from '@nestjs/common'; import { LOGGER_DRIVER } from './logger.constants'; @Injectable() -export class LoggerService implements ConsoleLoggerService { - constructor(@Inject(LOGGER_DRIVER) private driver: ConsoleLoggerService) {} +export class LoggerService implements LoggerServiceInterface { + constructor(@Inject(LOGGER_DRIVER) private driver: LoggerServiceInterface) {} log(message: any, category: string, ...optionalParams: any[]) { this.driver.log.apply(this.driver, [message, category, ...optionalParams]); @@ -41,4 +42,8 @@ export class LoggerService implements ConsoleLoggerService { ...optionalParams, ]); } + + setLogLevels(levels: LogLevel[]) { + this.driver.setLogLevels?.apply(this.driver, [levels]); + } } diff --git a/server/src/main.ts b/server/src/main.ts index a92fba6e1f..c8b4c4f6b6 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -9,6 +9,7 @@ import { AppModule } from './app.module'; import { settings } from './constants/settings'; import { LoggerService } from './integrations/logger/logger.service'; +import { EnvironmentService } from './integrations/environment/environment.service'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -35,8 +36,9 @@ async function bootstrap() { ); const loggerService = app.get(LoggerService); app.useLogger(loggerService); + app.useLogger(app.get(EnvironmentService).getLogLevels()); - await app.listen(3000); + await app.listen(app.get(EnvironmentService).getPort()); } bootstrap();