1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-21 15:36:26 +02:00

refactor: logger ()

This commit is contained in:
Jason Rasmussen 2025-02-27 14:59:50 -05:00 committed by GitHub
parent 1c86293035
commit fbd85a89e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 153 additions and 86 deletions

View file

@ -6,6 +6,7 @@ import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ClassConstructor } from 'class-transformer';
import { PostgresJSDialect } from 'kysely-postgres-js';
import { ClsModule } from 'nestjs-cls';
import { KyselyModule } from 'nestjs-kysely';
import { OpenTelemetryModule } from 'nestjs-otel';
import { mkdir, rm, writeFile } from 'node:fs/promises';
@ -77,7 +78,7 @@ class SqlGenerator {
await mkdir(this.options.targetDir);
process.env.DB_HOSTNAME = 'localhost';
const { database, otel } = new ConfigRepository().getEnv();
const { database, cls, otel } = new ConfigRepository().getEnv();
const moduleFixture = await Test.createTestingModule({
imports: [
@ -92,6 +93,7 @@ class SqlGenerator {
}
},
}),
ClsModule.forRoot(cls.config),
TypeOrmModule.forRoot({
...database.config.typeorm,
entities,

View file

@ -1,8 +1,8 @@
import { ClsService } from 'nestjs-cls';
import { ImmichWorker } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { LoggingRepository, MyConsoleLogger } from 'src/repositories/logging.repository';
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
import { Mocked } from 'vitest';
describe(LoggingRepository.name, () => {
@ -18,23 +18,25 @@ describe(LoggingRepository.name, () => {
} as unknown as Mocked<ClsService>;
});
describe('formatContext', () => {
it('should use colors', () => {
configMock.getEnv.mockReturnValue(mockEnvData({ noColor: false }));
describe(MyConsoleLogger.name, () => {
describe('formatContext', () => {
it('should use colors', () => {
sut = new LoggingRepository(clsMock, configMock);
sut.setAppName(ImmichWorker.API);
sut = new LoggingRepository(clsMock, configMock);
sut.setAppName(ImmichWorker.API);
const logger = new MyConsoleLogger(clsMock, { color: true });
expect(sut['formatContext']('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
});
expect(logger.formatContext('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
});
it('should not use colors when noColor is true', () => {
configMock.getEnv.mockReturnValue(mockEnvData({ noColor: true }));
it('should not use colors when color is false', () => {
sut = new LoggingRepository(clsMock, configMock);
sut.setAppName(ImmichWorker.API);
sut = new LoggingRepository(clsMock, configMock);
sut.setAppName(ImmichWorker.API);
const logger = new MyConsoleLogger(clsMock, { color: false });
expect(sut['formatContext']('context')).toBe('[Api:context] ');
expect(logger.formatContext('context')).toBe('[Api:context] ');
});
});
});
});

View file

@ -5,6 +5,9 @@ import { Telemetry } from 'src/decorators';
import { LogLevel } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
type LogDetails = any[];
type LogFunction = () => string;
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
enum LogColor {
@ -16,38 +19,22 @@ enum LogColor {
CYAN_BRIGHT = 96,
}
@Injectable({ scope: Scope.TRANSIENT })
@Telemetry({ enabled: false })
export class LoggingRepository extends ConsoleLogger {
private static logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
private noColor: boolean;
let appName: string | undefined;
let logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
export class MyConsoleLogger extends ConsoleLogger {
private isColorEnabled: boolean;
constructor(
private cls: ClsService,
configRepository: ConfigRepository,
options?: { color?: boolean; context?: string },
) {
super(LoggingRepository.name);
const { noColor } = configRepository.getEnv();
this.noColor = noColor;
super(options?.context || MyConsoleLogger.name);
this.isColorEnabled = options?.color || false;
}
private static appName?: string = undefined;
setAppName(name: string): void {
LoggingRepository.appName = name.charAt(0).toUpperCase() + name.slice(1);
}
isLevelEnabled(level: LogLevel) {
return isLogLevelEnabled(level, LoggingRepository.logLevels);
}
setLogLevel(level: LogLevel | false): void {
LoggingRepository.logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : [];
}
protected formatContext(context: string): string {
let prefix = LoggingRepository.appName || '';
formatContext(context: string): string {
let prefix = appName || '';
if (context) {
prefix += (prefix ? ':' : '') + context;
}
@ -74,6 +61,105 @@ export class LoggingRepository extends ConsoleLogger {
};
private withColor(text: string, color: LogColor) {
return this.noColor ? text : `\u001B[${color}m${text}\u001B[39m`;
return this.isColorEnabled ? `\u001B[${color}m${text}\u001B[39m` : text;
}
}
@Injectable({ scope: Scope.TRANSIENT })
@Telemetry({ enabled: false })
export class LoggingRepository {
private logger: MyConsoleLogger;
constructor(cls: ClsService, configRepository: ConfigRepository) {
const { noColor } = configRepository.getEnv();
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
}
setAppName(name: string): void {
appName = name.charAt(0).toUpperCase() + name.slice(1);
}
setContext(context: string) {
this.logger.setContext(context);
}
isLevelEnabled(level: LogLevel) {
return isLogLevelEnabled(level, logLevels);
}
setLogLevel(level: LogLevel | false): void {
logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : [];
}
verbose(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.VERBOSE, message, details);
}
verboseFn(message: LogFunction, ...details: LogDetails) {
this.handleFunction(LogLevel.VERBOSE, message, details);
}
debug(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.DEBUG, message, details);
}
debugFn(message: LogFunction, ...details: LogDetails) {
this.handleFunction(LogLevel.DEBUG, message, details);
}
log(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.LOG, message, details);
}
warn(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.WARN, message, details);
}
error(message: string | Error, ...details: LogDetails) {
this.handleMessage(LogLevel.ERROR, message, details);
}
fatal(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.FATAL, message, details);
}
private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) {
if (this.isLevelEnabled(level)) {
this.handleMessage(level, message(), details);
}
}
private handleMessage(level: LogLevel, message: string | Error, details: LogDetails[]) {
switch (level) {
case LogLevel.VERBOSE: {
this.logger.verbose(message, ...details);
break;
}
case LogLevel.DEBUG: {
this.logger.debug(message, ...details);
break;
}
case LogLevel.LOG: {
this.logger.log(message, ...details);
break;
}
case LogLevel.WARN: {
this.logger.warn(message, ...details);
break;
}
case LogLevel.ERROR: {
this.logger.error(message, ...details);
break;
}
case LogLevel.FATAL: {
this.logger.fatal(message, ...details);
break;
}
}
}
}

View file

@ -10,7 +10,7 @@ import { citiesFile } from 'src/constants';
import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
import { LogLevel, SystemMetadataKey } from 'src/enum';
import { SystemMetadataKey } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
@ -137,9 +137,7 @@ export class MapRepository {
.executeTakeFirst();
if (response) {
if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
}
this.logger.verboseFn(() => `Raw: ${JSON.stringify(response, null, 2)}`);
const { countryCode, name: city, admin1Name } = response;
const country = getName(countryCode, 'en') ?? null;
@ -167,9 +165,8 @@ export class MapRepository {
return { country: null, state: null, city: null };
}
if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
}
this.logger.verboseFn(() => `Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
const { admin_a3 } = ne_response;
const country = getName(admin_a3, 'en') ?? null;
const state = null;

View file

@ -1,16 +1,11 @@
import { LoggingRepository } from 'src/repositories/logging.repository';
import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository';
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest';
import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock';
describe(NotificationRepository.name, () => {
let sut: NotificationRepository;
let loggerMock: Mocked<LoggingRepository>;
beforeEach(() => {
loggerMock = newLoggingRepositoryMock() as ILoggingRepository as Mocked<LoggingRepository>;
sut = new NotificationRepository(loggerMock as LoggingRepository);
sut = new NotificationRepository(newFakeLoggingRepository());
});
describe('renderEmail', () => {

View file

@ -1,9 +1,7 @@
import mockfs from 'mock-fs';
import { CrawlOptionsDto } from 'src/dtos/library.dto';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest';
import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock';
interface Test {
test: string;
@ -182,11 +180,9 @@ const tests: Test[] = [
describe(StorageRepository.name, () => {
let sut: StorageRepository;
let logger: Mocked<ILoggingRepository>;
beforeEach(() => {
logger = newLoggingRepositoryMock();
sut = new StorageRepository(logger as ILoggingRepository as LoggingRepository);
sut = new StorageRepository(newFakeLoggingRepository());
});
afterEach(() => {

View file

@ -54,7 +54,7 @@ export class StorageService extends BaseService {
this.logger.log('Successfully verified system mount folder checks');
} catch (error) {
if (envData.storage.ignoreMountCheckErrors) {
this.logger.error(error);
this.logger.error(error as Error);
this.logger.warn('Ignoring mount folder errors');
} else {
throw error;

View file

@ -3,15 +3,12 @@ import { writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { AssetEntity } from 'src/entities/asset.entity';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service';
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock';
import { newRandomImage, newTestService, ServiceMocks } from 'test/utils';
const metadataRepository = new MetadataRepository(
newLoggingRepositoryMock() as ILoggingRepository as LoggingRepository,
);
const metadataRepository = new MetadataRepository(newFakeLoggingRepository());
const createTestFile = async (exifData: Record<string, any>) => {
const data = newRandomImage();

View file

@ -1,31 +1,23 @@
import { LoggingRepository } from 'src/repositories/logging.repository';
import { RepositoryInterface } from 'src/types';
import { Mocked, vitest } from 'vitest';
export type ILoggingRepository = Pick<
LoggingRepository,
| 'verbose'
| 'log'
| 'debug'
| 'warn'
| 'error'
| 'fatal'
| 'isLevelEnabled'
| 'setLogLevel'
| 'setContext'
| 'setAppName'
>;
export const newLoggingRepositoryMock = (): Mocked<ILoggingRepository> => {
export const newLoggingRepositoryMock = (): Mocked<RepositoryInterface<LoggingRepository>> => {
return {
setLogLevel: vitest.fn(),
setContext: vitest.fn(),
setAppName: vitest.fn(),
isLevelEnabled: vitest.fn(),
verbose: vitest.fn(),
verboseFn: vitest.fn(),
debug: vitest.fn(),
debugFn: vitest.fn(),
log: vitest.fn(),
warn: vitest.fn(),
error: vitest.fn(),
fatal: vitest.fn(),
};
};
export const newFakeLoggingRepository = () =>
newLoggingRepositoryMock() as RepositoryInterface<LoggingRepository> as LoggingRepository;

View file

@ -63,7 +63,7 @@ import { newDatabaseRepositoryMock } from 'test/repositories/database.repository
import { newEventRepositoryMock } from 'test/repositories/event.repository.mock';
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
import { newMapRepositoryMock } from 'test/repositories/map.repository.mock';
import { newMediaRepositoryMock } from 'test/repositories/media.repository.mock';
@ -120,7 +120,7 @@ export type ServiceMocks = {
event: Mocked<RepositoryInterface<EventRepository>>;
job: Mocked<RepositoryInterface<JobRepository>>;
library: Mocked<RepositoryInterface<LibraryRepository>>;
logger: Mocked<ILoggingRepository>;
logger: Mocked<RepositoryInterface<LoggingRepository>>;
machineLearning: Mocked<RepositoryInterface<MachineLearningRepository>>;
map: Mocked<RepositoryInterface<MapRepository>>;
media: Mocked<RepositoryInterface<MediaRepository>>;
@ -197,7 +197,7 @@ export const newTestService = <T extends BaseService>(
const viewMock = newViewRepositoryMock();
const sut = new Service(
loggerMock as ILoggingRepository as LoggingRepository,
loggerMock as RepositoryInterface<LoggingRepository> as LoggingRepository,
accessMock as IAccessRepository as AccessRepository,
activityMock as RepositoryInterface<ActivityRepository> as ActivityRepository,
auditMock as RepositoryInterface<AuditRepository> as AuditRepository,