mirror of
https://github.com/immich-app/immich.git
synced 2025-04-21 15:36:26 +02:00
refactor: logger (#16393)
This commit is contained in:
parent
1c86293035
commit
fbd85a89e0
10 changed files with 153 additions and 86 deletions
server
|
@ -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,
|
||||
|
|
|
@ -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] ');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue