diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts index 544e63d771c6a..bad4a83222de0 100644 --- a/server/src/interfaces/config.interface.ts +++ b/server/src/interfaces/config.interface.ts @@ -41,6 +41,7 @@ export interface EnvData { workers: ImmichWorker[]; + noColor: boolean; nodeVersion?: string; } diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index 2fbd76cefbcd7..83d89c6e01d14 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -1,51 +1,76 @@ import { ConfigRepository } from 'src/repositories/config.repository'; -const getWorkers = () => new ConfigRepository().getEnv().workers; +const getEnv = () => new ConfigRepository().getEnv(); -describe('getWorkers', () => { +describe('getEnv', () => { beforeEach(() => { - process.env.IMMICH_WORKERS_INCLUDE = ''; - process.env.IMMICH_WORKERS_EXCLUDE = ''; + delete process.env.IMMICH_WORKERS_INCLUDE; + delete process.env.IMMICH_WORKERS_EXCLUDE; + delete process.env.NO_COLOR; }); it('should return default workers', () => { - expect(getWorkers()).toEqual(['api', 'microservices']); + const { workers } = getEnv(); + expect(workers).toEqual(['api', 'microservices']); }); it('should return included workers', () => { process.env.IMMICH_WORKERS_INCLUDE = 'api'; - expect(getWorkers()).toEqual(['api']); + const { workers } = getEnv(); + expect(workers).toEqual(['api']); }); it('should excluded workers from defaults', () => { process.env.IMMICH_WORKERS_EXCLUDE = 'api'; - expect(getWorkers()).toEqual(['microservices']); + const { workers } = getEnv(); + expect(workers).toEqual(['microservices']); }); it('should exclude workers from include list', () => { process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice'; process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices'; - expect(getWorkers()).toEqual(['api']); + const { workers } = getEnv(); + expect(workers).toEqual(['api']); }); it('should remove whitespace from included workers before parsing', () => { process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices'; - expect(getWorkers()).toEqual(['api', 'microservices']); + const { workers } = getEnv(); + expect(workers).toEqual(['api', 'microservices']); }); it('should remove whitespace from excluded workers before parsing', () => { process.env.IMMICH_WORKERS_EXCLUDE = 'api, microservices'; - expect(getWorkers()).toEqual([]); + const { workers } = getEnv(); + expect(workers).toEqual([]); }); it('should remove whitespace from included and excluded workers before parsing', () => { process.env.IMMICH_WORKERS_INCLUDE = 'api, microservices, randomservice,randomservice2'; process.env.IMMICH_WORKERS_EXCLUDE = 'randomservice,microservices, randomservice2'; - expect(getWorkers()).toEqual(['api']); + const { workers } = getEnv(); + expect(workers).toEqual(['api']); }); it('should throw error for invalid workers', () => { process.env.IMMICH_WORKERS_INCLUDE = 'api,microservices,randomservice'; - expect(getWorkers).toThrowError('Invalid worker(s) found: api,microservices,randomservice'); + expect(getEnv).toThrowError('Invalid worker(s) found: api,microservices,randomservice'); + }); + + it('should default noColor to false', () => { + const { noColor } = getEnv(); + expect(noColor).toBe(false); + }); + + it('should map NO_COLOR=1 to true', () => { + process.env.NO_COLOR = '1'; + const { noColor } = getEnv(); + expect(noColor).toBe(true); + }); + + it('should map NO_COLOR=true to true', () => { + process.env.NO_COLOR = 'true'; + const { noColor } = getEnv(); + expect(noColor).toBe(true); }); }); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index ec80ed4db49f2..37013d851207e 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -68,11 +68,16 @@ export class ConfigRepository implements IConfigRepository { skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true', vectorExtension: getVectorExtension(), }, + licensePublicKey: isProd ? productionKeys : stagingKeys, + storage: { ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true', }, + workers, + + noColor: !!process.env.NO_COLOR, }; } } diff --git a/server/src/repositories/logger.repository.spec.ts b/server/src/repositories/logger.repository.spec.ts new file mode 100644 index 0000000000000..3093c090da8bd --- /dev/null +++ b/server/src/repositories/logger.repository.spec.ts @@ -0,0 +1,39 @@ +import { ClsService } from 'nestjs-cls'; +import { IConfigRepository } from 'src/interfaces/config.interface'; +import { LoggerRepository } from 'src/repositories/logger.repository'; +import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; +import { Mocked } from 'vitest'; + +describe(LoggerRepository.name, () => { + let sut: LoggerRepository; + + let configMock: Mocked; + let clsMock: Mocked; + + beforeEach(() => { + configMock = newConfigRepositoryMock(); + clsMock = { + getId: vitest.fn(), + } as unknown as Mocked; + }); + + describe('formatContext', () => { + it('should use colors', () => { + configMock.getEnv.mockReturnValue(mockEnvData({ noColor: false })); + + sut = new LoggerRepository(clsMock, configMock); + sut.setAppName('api'); + + expect(sut['formatContext']('context')).toBe('\u001B[33m[api:context]\u001B[39m '); + }); + + it('should not use colors when noColor is true', () => { + configMock.getEnv.mockReturnValue(mockEnvData({ noColor: true })); + + sut = new LoggerRepository(clsMock, configMock); + sut.setAppName('api'); + + expect(sut['formatContext']('context')).toBe('[api:context] '); + }); + }); +}); diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts index 08fb6e797325f..f1a99a85ed5e4 100644 --- a/server/src/repositories/logger.repository.ts +++ b/server/src/repositories/logger.repository.ts @@ -1,18 +1,34 @@ -import { ConsoleLogger, Injectable, Scope } from '@nestjs/common'; +import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common'; import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; import { ClsService } from 'nestjs-cls'; import { LogLevel } from 'src/enum'; +import { IConfigRepository } from 'src/interfaces/config.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { LogColor } from 'src/utils/logger'; const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; +enum LogColor { + RED = 31, + GREEN = 32, + YELLOW = 33, + BLUE = 34, + MAGENTA_BRIGHT = 95, + CYAN_BRIGHT = 96, +} + @Injectable({ scope: Scope.TRANSIENT }) export class LoggerRepository extends ConsoleLogger implements ILoggerRepository { private static logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; + private noColor: boolean; - constructor(private cls: ClsService) { + constructor( + private cls: ClsService, + @Inject(IConfigRepository) configRepository: IConfigRepository, + ) { super(LoggerRepository.name); + + const { noColor } = configRepository.getEnv(); + this.noColor = noColor; } private static appName?: string = undefined; @@ -44,6 +60,19 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository return ''; } - return LogColor.yellow(`[${prefix}]`) + ' '; + return this.colors.yellow(`[${prefix}]`) + ' '; + } + + private colors = { + red: (text: string) => this.withColor(text, LogColor.RED), + green: (text: string) => this.withColor(text, LogColor.GREEN), + yellow: (text: string) => this.withColor(text, LogColor.YELLOW), + blue: (text: string) => this.withColor(text, LogColor.BLUE), + magentaBright: (text: string) => this.withColor(text, LogColor.MAGENTA_BRIGHT), + cyanBright: (text: string) => this.withColor(text, LogColor.CYAN_BRIGHT), + }; + + private withColor(text: string, color: LogColor) { + return this.noColor ? text : `\u001B[${color}m${text}\u001B[39m`; } } diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index d4eb02ead21ab..2e33a7bcb557a 100644 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -2,24 +2,6 @@ import { HttpException } from '@nestjs/common'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { TypeORMError } from 'typeorm'; -type ColorTextFn = (text: string) => string; - -const isColorAllowed = () => !process.env.NO_COLOR; -const colorIfAllowed = (colorFn: ColorTextFn) => (text: string) => (isColorAllowed() ? colorFn(text) : text); - -export const LogColor = { - red: colorIfAllowed((text: string) => `\u001B[31m${text}\u001B[39m`), - green: colorIfAllowed((text: string) => `\u001B[32m${text}\u001B[39m`), - yellow: colorIfAllowed((text: string) => `\u001B[33m${text}\u001B[39m`), - blue: colorIfAllowed((text: string) => `\u001B[34m${text}\u001B[39m`), - magentaBright: colorIfAllowed((text: string) => `\u001B[95m${text}\u001B[39m`), - cyanBright: colorIfAllowed((text: string) => `\u001B[96m${text}\u001B[39m`), -}; - -export const LogStyle = { - bold: colorIfAllowed((text: string) => `\u001B[1m${text}\u001B[0m`), -}; - export const logGlobalError = (logger: ILoggerRepository, error: Error) => { if (error instanceof HttpException) { const status = error.getStatus(); diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts index 786e9adbcc2b0..42a113534e39a 100644 --- a/server/test/repositories/config.repository.mock.ts +++ b/server/test/repositories/config.repository.mock.ts @@ -24,6 +24,8 @@ const envData: EnvData = { }, workers: [ImmichWorker.API, ImmichWorker.MICROSERVICES], + + noColor: false, }; export const newConfigRepositoryMock = (): Mocked => {