From 9768931275234cdcfce3d3d000aff94031fb5f8d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 14 Dec 2023 11:55:40 -0500 Subject: [PATCH] feat(web,server)!: runtime log level (#5672) * feat: change log level at runtime * chore: open api * chore: prefer env over runtime * chore: remove default env value --- cli/src/api/open-api/api.ts | 45 +++++++ mobile/openapi/.openapi-generator/FILES | 6 + mobile/openapi/README.md | Bin 24067 -> 24159 bytes mobile/openapi/doc/LogLevel.md | Bin 0 -> 374 bytes mobile/openapi/doc/SystemConfigDto.md | Bin 1448 -> 1524 bytes mobile/openapi/doc/SystemConfigLoggingDto.md | Bin 0 -> 463 bytes mobile/openapi/lib/api.dart | Bin 7825 -> 7899 bytes mobile/openapi/lib/api_client.dart | Bin 22758 -> 22937 bytes mobile/openapi/lib/api_helper.dart | Bin 5736 -> 5830 bytes mobile/openapi/lib/model/log_level.dart | Bin 0 -> 2899 bytes .../openapi/lib/model/system_config_dto.dart | Bin 6314 -> 6575 bytes .../lib/model/system_config_logging_dto.dart | Bin 0 -> 3115 bytes mobile/openapi/test/log_level_test.dart | Bin 0 -> 413 bytes .../openapi/test/system_config_dto_test.dart | Bin 2043 -> 2158 bytes .../test/system_config_logging_dto_test.dart | Bin 0 -> 683 bytes server/immich-openapi-specs.json | 30 +++++ server/package.json | 3 - server/src/domain/asset/asset.service.ts | 5 +- server/src/domain/audit/audit.service.ts | 5 +- server/src/domain/auth/auth.service.ts | 4 +- server/src/domain/domain.config.ts | 16 +-- server/src/domain/domain.module.ts | 2 + server/src/domain/job/job.service.ts | 5 +- server/src/domain/library/library.service.ts | 5 +- server/src/domain/media/media.service.ts | 5 +- .../src/domain/metadata/metadata.service.ts | 5 +- server/src/domain/person/person.service.ts | 5 +- server/src/domain/search/search.service.ts | 5 +- .../domain/server-info/server-info.service.ts | 5 +- .../domain/smart-info/smart-info.service.ts | 5 +- .../storage-template.service.ts | 5 +- server/src/domain/storage/storage.core.ts | 4 +- server/src/domain/storage/storage.service.ts | 5 +- .../dto/system-config-logging.dto.ts | 12 ++ .../system-config/dto/system-config.dto.ts | 6 + .../system-config/system-config.core.ts | 28 +++-- .../system-config.service.spec.ts | 9 +- .../system-config/system-config.service.ts | 33 +++++- server/src/domain/user/user.service.ts | 5 +- .../src/immich/api-v1/asset/asset.service.ts | 5 +- server/src/immich/app.guard.ts | 4 +- server/src/immich/app.service.ts | 8 +- .../immich/interceptors/error.interceptor.ts | 4 +- .../interceptors/file-serve.interceptor.ts | 5 +- .../interceptors/file-upload.interceptor.ts | 5 +- server/src/immich/main.ts | 9 +- .../infra/entities/system-config.entity.ts | 22 +++- server/src/infra/logger.ts | 21 ++++ .../repositories/communication.repository.ts | 4 +- .../infra/repositories/filesystem.provider.ts | 4 +- .../src/infra/repositories/job.repository.ts | 5 +- .../infra/repositories/media.repository.ts | 4 +- .../infra/repositories/metadata.repository.ts | 5 +- .../repositories/smart-info.repository.ts | 5 +- server/src/microservices/app.service.ts | 10 +- server/src/microservices/main.ts | 11 +- .../src/microservices/microservices.module.ts | 10 +- server/test/setup.ts | 11 -- web/src/api/open-api/api.ts | 45 +++++++ .../logging-settings/logging-settings.svelte | 110 ++++++++++++++++++ .../routes/admin/system-settings/+page.svelte | 5 + 61 files changed, 459 insertions(+), 116 deletions(-) create mode 100644 mobile/openapi/doc/LogLevel.md create mode 100644 mobile/openapi/doc/SystemConfigLoggingDto.md create mode 100644 mobile/openapi/lib/model/log_level.dart create mode 100644 mobile/openapi/lib/model/system_config_logging_dto.dart create mode 100644 mobile/openapi/test/log_level_test.dart create mode 100644 mobile/openapi/test/system_config_logging_dto_test.dart create mode 100644 server/src/domain/system-config/dto/system-config-logging.dto.ts create mode 100644 server/src/infra/logger.ts delete mode 100644 server/test/setup.ts create mode 100644 web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index e74361de77..965030af4b 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -2175,6 +2175,24 @@ export const LibraryType = { export type LibraryType = typeof LibraryType[keyof typeof LibraryType]; +/** + * + * @export + * @enum {string} + */ + +export const LogLevel = { + Verbose: 'verbose', + Debug: 'debug', + Log: 'log', + Warn: 'warn', + Error: 'error', + Fatal: 'fatal' +} as const; + +export type LogLevel = typeof LogLevel[keyof typeof LogLevel]; + + /** * * @export @@ -3577,6 +3595,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'library': SystemConfigLibraryDto; + /** + * + * @type {SystemConfigLoggingDto} + * @memberof SystemConfigDto + */ + 'logging': SystemConfigLoggingDto; /** * * @type {SystemConfigMachineLearningDto} @@ -3860,6 +3884,27 @@ export interface SystemConfigLibraryScanDto { */ 'enabled': boolean; } +/** + * + * @export + * @interface SystemConfigLoggingDto + */ +export interface SystemConfigLoggingDto { + /** + * + * @type {boolean} + * @memberof SystemConfigLoggingDto + */ + 'enabled': boolean; + /** + * + * @type {LogLevel} + * @memberof SystemConfigLoggingDto + */ + 'level': LogLevel; +} + + /** * * @export diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 1d72f22499..5c98b2958a 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -82,6 +82,7 @@ doc/LibraryApi.md doc/LibraryResponseDto.md doc/LibraryStatsResponseDto.md doc/LibraryType.md +doc/LogLevel.md doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md @@ -142,6 +143,7 @@ doc/SystemConfigFFmpegDto.md doc/SystemConfigJobDto.md doc/SystemConfigLibraryDto.md doc/SystemConfigLibraryScanDto.md +doc/SystemConfigLoggingDto.md doc/SystemConfigMachineLearningDto.md doc/SystemConfigMapDto.md doc/SystemConfigNewVersionCheckDto.md @@ -274,6 +276,7 @@ lib/model/job_status_dto.dart lib/model/library_response_dto.dart lib/model/library_stats_response_dto.dart lib/model/library_type.dart +lib/model/log_level.dart lib/model/login_credential_dto.dart lib/model/login_response_dto.dart lib/model/logout_response_dto.dart @@ -327,6 +330,7 @@ lib/model/system_config_f_fmpeg_dto.dart lib/model/system_config_job_dto.dart lib/model/system_config_library_dto.dart lib/model/system_config_library_scan_dto.dart +lib/model/system_config_logging_dto.dart lib/model/system_config_machine_learning_dto.dart lib/model/system_config_map_dto.dart lib/model/system_config_new_version_check_dto.dart @@ -439,6 +443,7 @@ test/library_api_test.dart test/library_response_dto_test.dart test/library_stats_response_dto_test.dart test/library_type_test.dart +test/log_level_test.dart test/login_credential_dto_test.dart test/login_response_dto_test.dart test/logout_response_dto_test.dart @@ -499,6 +504,7 @@ test/system_config_f_fmpeg_dto_test.dart test/system_config_job_dto_test.dart test/system_config_library_dto_test.dart test/system_config_library_scan_dto_test.dart +test/system_config_logging_dto_test.dart test/system_config_machine_learning_dto_test.dart test/system_config_map_dto_test.dart test/system_config_new_version_check_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e69cfce6ee246e1c10db17f202e4b97c868d6954..6f398402610f15d4579c65c56a867430089edcd4 100644 GIT binary patch delta 65 zcmZqP!+3uWB6UeV_bvFn4p5_f0MqATNEgeV8OWh>@9>KAAB> OVsb>7&}P@Lee3`(hZnv8 delta 19 bcmcb=hp~ANA6g@1ABCU7QfntrJRs4A@4Sd;_du)}EDAG0TDn#P(W;C$pHIJ(Z?uS%1mRtTey zLLHRWDKC>a>S1L(;R(O~Vrg^g+vK7+4jHXjGDF=}9^N6PUSWXkc9#D{VO*9(*tuSI zyEm;To5gg#Txf4A%k9xq5L0~BHzzeD?8kV!o~}Rtd7JYp3O2F}@*r_Bd@>&cz!`GU Ba{mAT literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index 73c5b70dcfee85759f9a9444897fd13356161dbc..403260659a6c141448b532536523f378bbfcbf4a 100644 GIT binary patch delta 40 rcmZ3%{e^qO21d4={Pgt9yovWkS$u%3$@dw>APhz((aoC}zcB*>ZS>}Rh618UWsVps0~PyczB$4h`|MNiGTa^ zZ`m18K(B)>92xA3r}G%m-IG7ruA;89zAGaMk!4VkHV|JBCK7@^zUmG|+qOmzQCJsf zh_JZiZ!W&8pNZT$C!w;N_NL&rCVKI*iBI?TnO5`J2lJVE{Iu8SVf8 delta 23 fcmbQaneo|1#tl+Nn{$m;2~A$_~1GAT~uk-W~7Wz4LfH7?0`tY5w@*Z?lKlU)S^54c*?|&qj3fNDq&5`t*2ncl-An zz!>>47uHYSB|pC#@MF2w#>%uXHZ4jeuc*?MlX)tY+)C|7&2zck8SBrU5NaY~hr_Zcr47g3S!b%$(xuAiHDG=tbo$Fr zX|@s84F+)M$rNOEQj1-g;Q#$#Fvyf}j_%F+UOr1jUk5~E-25)OErpjPNXtD*U2SP! z`VIMwq@6X-2q~wCO8*jZA~TV&~HkYg!h+l6%EgExYD__>QL%6g|{3QW229) z+^(c$gGH%@qR-y8$YAE9d6w3~J0SH-Z{lKjNs|xJXn?k_|8PckUm~%-%<8aVa%G&P z$>{~`MZJB23lP0JkjquI?!v?78L%=vD7MZ3_rmI~QQkWPMGlPZV#DSca3MTW*|}WU zWN=-Z`_cu92&HIeoGVvK#$-Awbu7_>d9YkGgb9Uzm6uRa2Uou~m=6osC#q993J)Ps zafw941>ZBS!Y$%5#K_BK2(xz8n4VH$%{CM_KcKBp&d4{WK?9l}c5-focIe}+w8P6J zmzl}s@PwL;U1Z=G_mmW#Z@V7;0hdPfb!CjAwKaReX_`E%g{p&d98Gn@~^Xj zn{YnLf`)90K>U#Tw{MA`lhQpHKils7p|J&PDD^P*13Ypy728?Qs*{5I2K6E zuA}paeKR3lDRpX!D2APgJa3Z}&k?#)G04DplhkXf|7**|~_OQxi^6{H_IWw}3Z4|USU z(@IiIdrq#(GU;3aBL{9;t~1W-Kryx&nIiLqem8qe96RDZ=(rA@^nKs8?f;1&`ersg zY0T(2wPikI<&2lype^+4RPcoA1cW#bgmIc5XwSES#p^%s2$L0)7Wqb&PKIRcD_j=s zg9(YwwQ`G@l{Xn-Jbp)5AgshxDWSKO;~@vbM#0!_cO136!L7hiHZ+*l86P%v<^ngG z)YcKQ!&Pi0i4})|)a!YN!Jcj6+fyaT1V-X<3SQOqYvV|2QICqo5>cOsgRqYe`#rS6 zEbL>k+e3y~*oX3V4;yD;9~}03=r|1f=&*l8=dut`^eYsj`Qx;{X_J1MK1O(^7+u?S z#XlJX^@bNu`ilnf;t!t47c^(=qAU7=ixh`=Vv#0tW|=)7oAR42y5YPXPsPq)Mc&~*Ltx~<>Jz5cjEl! zUch3n$p+Yxm)`3KX$-;@?D4=xIBj4sh{pF{Wri@gr8MU-7=nj26GcR|#9`;#H~$IL zNpN1j=7POlI0WVh^}(?L7f9Rfm8#OgLONIqyZor<5^Lk|wFpUjrOc$Y!t6*pD2JuA z%CcEhfszFObNI<`aJs|t6Nv7dLAJqZ{~=;jy6yB$t1N?8fir)a!7I)(>vHrPv3}=+ z#W3HY)>Ssd_@AvVF}hbku1ZnBS6qStJh6V>6>-W-*bF%ubg5^i2*@mka#IvuEL&x{ zM;hF&jm)uSx!cVqL$%tJbev-IJC&t4Czg6nT>FI+{q-AH!bn%h`w9c>SX9LIk*=|< zunurU2%99Cu&5g<8x-VP(7zFnSP;w41`QuG>>gXTDBig8~y7R1!`MR%oK^t#+f;+U-JQBnSss zR3*?`oV9Fv*bfwXdIu*2a=;J|QNcx!9fWZ2Ohitqz9;D2;Ai#`r?_f%fmOUsa4Jh1&O{9PbG?*Z4wtZXgRAebjG*C!$2yXK zxy`+KcY3C~cCu62{);qfa?KE_J>D|f5h5|^mE=oQk=$Qy5rzSU^Odjp;5QARNj zCb@Pzi$e>j-F}$p!F-B8O&nEm4O}$VqFOgm~b{o^;yUN1T9n;>a-yg zts&8>9yc)R(P1O7#{soZC#?o+$j>@t{~a3h4f>0vthl`=tOUH7chU4THM`;`XtFQ_WMRitgPQ;hZ|#2ORIP|tN80= z`w#KxJdb4pVrbM3!b+epU4dup+1aQD2C4L7315r>bYw=mNFw@2B`Hkp%W})ug?$75 CE{iz; literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index c8b5c0d9c1bc8885a6db747abf33606c49b558df..5398f77601a161d283e51abaeeb9e430be1f2a2b 100644 GIT binary patch delta 43 pcmey(|4v|oCbN)FetLRlUb;(3zCsR!IoXg^ksZq3{DFy^6#!w(4|xCp delta 12 TcmaDS@SA^wCiCVw%$HaIB#H$M diff --git a/mobile/openapi/test/system_config_logging_dto_test.dart b/mobile/openapi/test/system_config_logging_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..cc638f53105c2b5ff90a7ce63fa25bf89ea9c9a2 GIT binary patch literal 683 zcma)2Pfx-y6u<9NJWoJ^OgR||iNt{+L6+z|cq(Ok8;xB%+YUnv-`&>&Cn0*+YhUyG zyEu;G7$$F7di^wAPG2V3G=bTCG3`T=!ZOX^IZfuX_bY;V&`l?AGIn)wa`^1RFya#ZM7(%=0bIQ@ErjVrxc1y5TQzr zFup+T_C^sADx=$`+wn#hfSo=Z#e@7>YYEh5EwLazgp4Gh!v)+1p)u6gxE-vHxYa>} o+nvfoOwXgJS(>ClV}A$}9;w{|w/test/setup.ts" - ], "testEnvironment": "node", "moduleNameMapper": { "^@test(|/.*)$": "/test/$1", diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 340ddb1edb..15893b0921 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -1,5 +1,6 @@ import { AssetEntity, LibraryType } from '@app/infra/entities'; -import { BadRequestException, Inject, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; import { extname } from 'path'; @@ -75,7 +76,7 @@ export interface UploadFile { } export class AssetService { - private logger = new Logger(AssetService.name); + private logger = new ImmichLogger(AssetService.name); private access: AccessCore; private configCore: SystemConfigCore; diff --git a/server/src/domain/audit/audit.service.ts b/server/src/domain/audit/audit.service.ts index f994843195..bd4d456ddd 100644 --- a/server/src/domain/audit/audit.service.ts +++ b/server/src/domain/audit/audit.service.ts @@ -1,5 +1,6 @@ import { AssetPathType, DatabaseAction, PersonPathType, UserPathType } from '@app/infra/entities'; -import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AccessCore, Permission } from '../access'; @@ -29,7 +30,7 @@ import { @Injectable() export class AuditService { private access: AccessCore; - private logger = new Logger(AuditService.name); + private logger = new ImmichLogger(AuditService.name); constructor( @Inject(IAccessRepository) accessRepository: IAccessRepository, diff --git a/server/src/domain/auth/auth.service.ts b/server/src/domain/auth/auth.service.ts index a18b312ba3..fd527ee0d9 100644 --- a/server/src/domain/auth/auth.service.ts +++ b/server/src/domain/auth/auth.service.ts @@ -1,10 +1,10 @@ import { SystemConfig, UserEntity } from '@app/infra/entities'; +import { ImmichLogger } from '@app/infra/logger'; import { BadRequestException, Inject, Injectable, InternalServerErrorException, - Logger, UnauthorizedException, } from '@nestjs/common'; import cookieParser from 'cookie'; @@ -68,7 +68,7 @@ interface OAuthProfile extends UserinfoResponse { export class AuthService { private access: AccessCore; private configCore: SystemConfigCore; - private logger = new Logger(AuthService.name); + private logger = new ImmichLogger(AuthService.name); private userCore: UserCore; constructor( diff --git a/server/src/domain/domain.config.ts b/server/src/domain/domain.config.ts index 318f2a2b73..3a106bad2b 100644 --- a/server/src/domain/domain.config.ts +++ b/server/src/domain/domain.config.ts @@ -1,5 +1,5 @@ // TODO: remove nestjs references from domain -import { LogLevel } from '@nestjs/common'; +import { LogLevel } from '@app/infra/entities'; import { ConfigModuleOptions } from '@nestjs/config'; import Joi from 'joi'; @@ -18,19 +18,11 @@ export const immichAppConfig: ConfigModuleOptions = { DB_PASSWORD: WHEN_DB_URL_SET, DB_DATABASE_NAME: WHEN_DB_URL_SET, DB_URL: Joi.string().optional(), - LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose', 'debug', 'log', 'warn', 'error').default('log'), + LOG_LEVEL: Joi.string() + .optional() + .valid(...Object.values(LogLevel)), MACHINE_LEARNING_PORT: Joi.number().optional(), MICROSERVICES_PORT: Joi.number().optional(), SERVER_PORT: Joi.number().optional(), }), }; - -export function getLogLevels() { - const LOG_LEVELS: LogLevel[] = ['verbose', 'debug', 'log', 'warn', 'error']; - let logLevel = process.env.LOG_LEVEL || 'log'; - if (logLevel === 'simple') { - logLevel = 'log'; - } - const logLevelIndex = LOG_LEVELS.indexOf(logLevel as LogLevel); - return logLevelIndex === -1 ? [] : LOG_LEVELS.slice(logLevelIndex); -} diff --git a/server/src/domain/domain.module.ts b/server/src/domain/domain.module.ts index 3f40b924f8..5851d3a908 100644 --- a/server/src/domain/domain.module.ts +++ b/server/src/domain/domain.module.ts @@ -1,3 +1,4 @@ +import { ImmichLogger } from '@app/infra/logger'; import { DynamicModule, Global, Module, ModuleMetadata, Provider } from '@nestjs/common'; import { ActivityService } from './activity'; import { AlbumService } from './album'; @@ -43,6 +44,7 @@ const providers: Provider[] = [ SystemConfigService, TagService, UserService, + ImmichLogger, { provide: INITIAL_SYSTEM_CONFIG, inject: [SystemConfigService], diff --git a/server/src/domain/job/job.service.ts b/server/src/domain/job/job.service.ts index c8976c02a6..0e266c4c41 100644 --- a/server/src/domain/job/job.service.ts +++ b/server/src/domain/job/job.service.ts @@ -1,5 +1,6 @@ import { AssetType } from '@app/infra/entities'; -import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { mapAsset } from '../asset'; import { ClientEvent, @@ -18,7 +19,7 @@ import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from './job.dto' @Injectable() export class JobService { - private logger = new Logger(JobService.name); + private logger = new ImmichLogger(JobService.name); private configCore: SystemConfigCore; constructor( diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 3c31482f33..ac4bd065d9 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -1,5 +1,5 @@ import { AssetType, LibraryType } from '@app/infra/entities'; -import { BadRequestException, Inject, Injectable, Logger } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; import path from 'node:path'; @@ -10,6 +10,7 @@ import { mimeTypes } from '../domain.constant'; import { usePagination, validateCronExpression } from '../domain.util'; import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; +import { ImmichLogger } from '@app/infra/logger'; import { IAccessRepository, IAssetRepository, @@ -33,7 +34,7 @@ import { @Injectable() export class LibraryService { - readonly logger = new Logger(LibraryService.name); + readonly logger = new ImmichLogger(LibraryService.name); private access: AccessCore; private configCore: SystemConfigCore; diff --git a/server/src/domain/media/media.service.ts b/server/src/domain/media/media.service.ts index 4ddeca1d3f..463bff4826 100644 --- a/server/src/domain/media/media.service.ts +++ b/server/src/domain/media/media.service.ts @@ -7,7 +7,8 @@ import { TranscodePolicy, VideoCodec, } from '@app/infra/entities'; -import { Inject, Injectable, Logger, UnsupportedMediaTypeException } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; import { usePagination } from '../domain.util'; import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; import { @@ -39,7 +40,7 @@ import { @Injectable() export class MediaService { - private logger = new Logger(MediaService.name); + private logger = new ImmichLogger(MediaService.name); private configCore: SystemConfigCore; private storageCore: StorageCore; diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index e160eda636..13e6110af8 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -1,5 +1,6 @@ import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; import { ExifDateTime, Tags } from 'exiftool-vendored'; import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime'; import { constants } from 'fs/promises'; @@ -91,7 +92,7 @@ const validate = (value: T): NonNullable | null => { @Injectable() export class MetadataService { - private logger = new Logger(MetadataService.name); + private logger = new ImmichLogger(MetadataService.name); private storageCore: StorageCore; private configCore: SystemConfigCore; private subscription: Subscription | null = null; diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index b2d4b9c34f..836a3bf2da 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -1,6 +1,7 @@ import { PersonEntity } from '@app/infra/entities'; import { PersonPathType } from '@app/infra/entities/move.entity'; -import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { AssetResponseDto, BulkIdErrorReason, BulkIdResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; @@ -45,7 +46,7 @@ export class PersonService { private access: AccessCore; private configCore: SystemConfigCore; private storageCore: StorageCore; - readonly logger = new Logger(PersonService.name); + readonly logger = new ImmichLogger(PersonService.name); constructor( @Inject(IAccessRepository) accessRepository: IAccessRepository, diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index cd454ad17d..0bceb43578 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,5 +1,6 @@ import { AssetEntity } from '@app/infra/entities'; -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; import { AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; import { PersonResponseDto } from '../person'; @@ -18,7 +19,7 @@ import { SearchResponseDto } from './response-dto'; @Injectable() export class SearchService { - private logger = new Logger(SearchService.name); + private logger = new ImmichLogger(SearchService.name); private configCore: SystemConfigCore; constructor( diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index 291bb32cf0..014dbfc8da 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -1,4 +1,5 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { ServerVersion, isDev, mimeTypes, serverVersion } from '../domain.constant'; import { asHumanReadable } from '../domain.util'; @@ -25,7 +26,7 @@ import { @Injectable() export class ServerInfoService { - private logger = new Logger(ServerInfoService.name); + private logger = new ImmichLogger(ServerInfoService.name); private configCore: SystemConfigCore; private releaseVersion = serverVersion; private releaseVersionCheckedAt: DateTime | null = null; diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts index d9157c2be4..88208dec9f 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/domain/smart-info/smart-info.service.ts @@ -1,4 +1,5 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; import { setTimeout } from 'timers/promises'; import { usePagination } from '../domain.util'; import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; @@ -15,7 +16,7 @@ import { SystemConfigCore } from '../system-config'; @Injectable() export class SmartInfoService { private configCore: SystemConfigCore; - private logger = new Logger(SmartInfoService.name); + private logger = new ImmichLogger(SmartInfoService.name); constructor( @Inject(IAssetRepository) private assetRepository: IAssetRepository, diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts index 8e8bd81ea9..cbaf554112 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/domain/storage-template/storage-template.service.ts @@ -1,5 +1,6 @@ import { AssetEntity, AssetPathType, AssetType, SystemConfig } from '@app/infra/entities'; -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; import handlebar from 'handlebars'; import * as luxon from 'luxon'; import path from 'node:path'; @@ -42,7 +43,7 @@ interface RenderMetadata { @Injectable() export class StorageTemplateService { - private logger = new Logger(StorageTemplateService.name); + private logger = new ImmichLogger(StorageTemplateService.name); private configCore: SystemConfigCore; private storageCore: StorageCore; private template: { diff --git a/server/src/domain/storage/storage.core.ts b/server/src/domain/storage/storage.core.ts index c78e3b0424..6a6e83087a 100644 --- a/server/src/domain/storage/storage.core.ts +++ b/server/src/domain/storage/storage.core.ts @@ -1,5 +1,5 @@ import { AssetEntity, AssetPathType, PathType, PersonEntity, PersonPathType } from '@app/infra/entities'; -import { Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; import { dirname, join, resolve } from 'node:path'; import { APP_MEDIA_LOCATION } from '../domain.constant'; import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories'; @@ -24,7 +24,7 @@ type GeneratedAssetPath = AssetPathType.JPEG_THUMBNAIL | AssetPathType.WEBP_THUM let instance: StorageCore | null; export class StorageCore { - private logger = new Logger(StorageCore.name); + private logger = new ImmichLogger(StorageCore.name); private constructor( private assetRepository: IAssetRepository, diff --git a/server/src/domain/storage/storage.service.ts b/server/src/domain/storage/storage.service.ts index 0d7c9432e1..994a2b6fd5 100644 --- a/server/src/domain/storage/storage.service.ts +++ b/server/src/domain/storage/storage.service.ts @@ -1,11 +1,12 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; import { IDeleteFilesJob } from '../job'; import { IStorageRepository } from '../repositories'; import { StorageCore, StorageFolder } from './storage.core'; @Injectable() export class StorageService { - private logger = new Logger(StorageService.name); + private logger = new ImmichLogger(StorageService.name); constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {} diff --git a/server/src/domain/system-config/dto/system-config-logging.dto.ts b/server/src/domain/system-config/dto/system-config-logging.dto.ts new file mode 100644 index 0000000000..d280df5356 --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-logging.dto.ts @@ -0,0 +1,12 @@ +import { LogLevel } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsEnum } from 'class-validator'; + +export class SystemConfigLoggingDto { + @IsBoolean() + enabled!: boolean; + + @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) + @IsEnum(LogLevel) + level!: LogLevel; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index dbd45855ca..6fbfeced2b 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -4,6 +4,7 @@ import { IsObject, ValidateNested } from 'class-validator'; import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto'; import { SystemConfigJobDto } from './system-config-job.dto'; import { SystemConfigLibraryDto } from './system-config-library.dto'; +import { SystemConfigLoggingDto } from './system-config-logging.dto'; import { SystemConfigMachineLearningDto } from './system-config-machine-learning.dto'; import { SystemConfigMapDto } from './system-config-map.dto'; import { SystemConfigNewVersionCheckDto } from './system-config-new-version-check.dto'; @@ -21,6 +22,11 @@ export class SystemConfigDto implements SystemConfig { @IsObject() ffmpeg!: SystemConfigFFmpegDto; + @Type(() => SystemConfigLoggingDto) + @ValidateNested() + @IsObject() + logging!: SystemConfigLoggingDto; + @Type(() => SystemConfigMachineLearningDto) @ValidateNested() @IsObject() diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 7433bac79f..5ec523afa0 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -2,6 +2,7 @@ import { AudioCodec, Colorspace, CQMode, + LogLevel, SystemConfig, SystemConfigEntity, SystemConfigKey, @@ -11,7 +12,8 @@ import { TranscodePolicy, VideoCodec, } from '@app/infra/entities'; -import { BadRequestException, ForbiddenException, Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { CronExpression } from '@nestjs/schedule'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; @@ -21,7 +23,7 @@ import { QueueName } from '../job/job.constants'; import { ISystemConfigRepository } from '../repositories'; import { SystemConfigDto } from './dto'; -export type SystemConfigValidator = (config: SystemConfig) => void | Promise; +export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise; export const defaults = Object.freeze({ ffmpeg: { @@ -57,6 +59,10 @@ export const defaults = Object.freeze({ [QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 }, [QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, }, + logging: { + enabled: true, + level: LogLevel.LOG, + }, machineLearning: { enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003', @@ -149,7 +155,7 @@ let instance: SystemConfigCore | null; @Injectable() export class SystemConfigCore { - private logger = new Logger(SystemConfigCore.name); + private logger = new ImmichLogger(SystemConfigCore.name); private validators: SystemConfigValidator[] = []; private configCache: SystemConfigEntity[] | null = null; @@ -253,14 +259,16 @@ export class SystemConfigCore { return config; } - public async updateConfig(config: SystemConfig): Promise { + public async updateConfig(newConfig: SystemConfig): Promise { if (await this.hasFeature(FeatureFlag.CONFIG_FILE)) { throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use'); } + const oldConfig = await this.getConfig(); + try { for (const validator of this.validators) { - await validator(config); + await validator(newConfig, oldConfig); } } catch (e) { this.logger.warn(`Unable to save system config due to a validation error: ${e}`); @@ -272,9 +280,9 @@ export class SystemConfigCore { for (const key of Object.values(SystemConfigKey)) { // get via dot notation - const item = { key, value: _.get(config, key) as SystemConfigValue }; + const item = { key, value: _.get(newConfig, key) as SystemConfigValue }; const defaultValue = _.get(defaults, key); - const isMissing = !_.has(config, key); + const isMissing = !_.has(newConfig, key); if ( isMissing || @@ -298,11 +306,11 @@ export class SystemConfigCore { await this.repository.deleteKeys(deletes.map((item) => item.key)); } - const newConfig = await this.getConfig(); + const config = await this.getConfig(); - this.config$.next(newConfig); + this.config$.next(config); - return newConfig; + return config; } public async refreshConfig() { diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 6d1aa503d3..c67fb9e4ca 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -2,6 +2,7 @@ import { AudioCodec, Colorspace, CQMode, + LogLevel, SystemConfig, SystemConfigEntity, SystemConfigKey, @@ -57,6 +58,10 @@ const updatedConfig = Object.freeze({ accel: TranscodeHWAccel.DISABLED, tonemap: ToneMapping.HABLE, }, + logging: { + enabled: true, + level: LogLevel.LOG, + }, machineLearning: { enabled: true, url: 'http://immich-machine-learning:3003', @@ -159,7 +164,7 @@ describe(SystemConfigService.name, () => { const validator: SystemConfigValidator = jest.fn(); sut.addValidator(validator); await sut.updateConfig(defaults); - expect(validator).toHaveBeenCalledWith(defaults); + expect(validator).toHaveBeenCalledWith(defaults, defaults); }); }); @@ -279,7 +284,7 @@ describe(SystemConfigService.name, () => { await expect(sut.updateConfig(updatedConfig)).rejects.toBeInstanceOf(BadRequestException); - expect(validator).toHaveBeenCalledWith(updatedConfig); + expect(validator).toHaveBeenCalledWith(updatedConfig, defaults); expect(configMock.saveAll).not.toHaveBeenCalled(); }); diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 12c78101ee..5de4c93987 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -1,4 +1,8 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; +import { LogLevel, SystemConfig } from '@app/infra/entities'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable } from '@nestjs/common'; +import { instanceToPlain } from 'class-transformer'; +import _ from 'lodash'; import { ClientEvent, ICommunicationRepository, @@ -22,7 +26,7 @@ import { SystemConfigCore, SystemConfigValidator } from './system-config.core'; @Injectable() export class SystemConfigService { - private logger = new Logger(SystemConfigService.name); + private logger = new ImmichLogger(SystemConfigService.name); private core: SystemConfigCore; constructor( @@ -32,6 +36,13 @@ export class SystemConfigService { ) { this.core = SystemConfigCore.create(repository); this.communicationRepository.on(ServerEvent.CONFIG_UPDATE, () => this.handleConfigUpdate()); + this.core.config$.subscribe((config) => this.setLogLevel(config)); + this.core.addValidator((newConfig, oldConfig) => this.validateConfig(newConfig, oldConfig)); + } + + async init() { + const config = await this.core.getConfig(); + await this.setLogLevel(config); } get config$() { @@ -106,4 +117,22 @@ export class SystemConfigService { private async handleConfigUpdate() { await this.core.refreshConfig(); } + + private async setLogLevel({ logging }: SystemConfig) { + const envLevel = this.getEnvLogLevel(); + const configLevel = logging.enabled ? logging.level : false; + const level = envLevel ? envLevel : configLevel; + ImmichLogger.setLogLevel(level); + this.logger.log(`LogLevel=${level} ${envLevel ? '(set via LOG_LEVEL)' : '(set via system config)'}`); + } + + private getEnvLogLevel() { + return process.env.LOG_LEVEL as LogLevel; + } + + private async validateConfig(newConfig: SystemConfig, oldConfig: SystemConfig) { + if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { + throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.'); + } + } } diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index 8cc6580293..dde61711a1 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -1,5 +1,6 @@ import { UserEntity } from '@app/infra/entities'; -import { BadRequestException, ForbiddenException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { randomBytes } from 'crypto'; import { AuthDto } from '../auth'; import { ImmichFileResponse } from '../domain.util'; @@ -21,7 +22,7 @@ import { UserCore } from './user.core'; @Injectable() export class UserService { - private logger = new Logger(UserService.name); + private logger = new ImmichLogger(UserService.name); private userCore: UserCore; constructor( diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index c3742a3475..bc8ff3b63f 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -15,7 +15,8 @@ import { UploadFile, } from '@app/domain'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType, LibraryType } from '@app/infra/entities'; -import { Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { QueryFailedError } from 'typeorm'; import { IAssetRepository } from './asset-repository'; import { AssetCore } from './asset.core'; @@ -38,7 +39,7 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon @Injectable() export class AssetService { - readonly logger = new Logger(AssetService.name); + readonly logger = new ImmichLogger(AssetService.name); private assetCore: AssetCore; private access: AccessCore; diff --git a/server/src/immich/app.guard.ts b/server/src/immich/app.guard.ts index da802ba4a3..85f0689a8c 100644 --- a/server/src/immich/app.guard.ts +++ b/server/src/immich/app.guard.ts @@ -1,9 +1,9 @@ import { AuthDto, AuthService, IMMICH_API_KEY_NAME, LoginDetails } from '@app/domain'; +import { ImmichLogger } from '@app/infra/logger'; import { CanActivate, ExecutionContext, Injectable, - Logger, SetMetadata, applyDecorators, createParamDecorator, @@ -77,7 +77,7 @@ export interface AuthRequest extends Request { @Injectable() export class AppGuard implements CanActivate { - private logger = new Logger(AppGuard.name); + private logger = new ImmichLogger(AppGuard.name); constructor( private reflector: Reflector, diff --git a/server/src/immich/app.service.ts b/server/src/immich/app.service.ts index 9a6a5441eb..bc67e32048 100644 --- a/server/src/immich/app.service.ts +++ b/server/src/immich/app.service.ts @@ -6,8 +6,10 @@ import { ServerInfoService, SharedLinkService, StorageService, + SystemConfigService, } from '@app/domain'; -import { Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Injectable } from '@nestjs/common'; import { Cron, CronExpression, Interval } from '@nestjs/schedule'; import { NextFunction, Request, Response } from 'express'; import { readFileSync } from 'fs'; @@ -34,10 +36,11 @@ const render = (index: string, meta: OpenGraphTags) => { @Injectable() export class AppService { - private logger = new Logger(AppService.name); + private logger = new ImmichLogger(AppService.name); constructor( private authService: AuthService, + private configService: SystemConfigService, private jobService: JobService, private serverService: ServerInfoService, private sharedLinkService: SharedLinkService, @@ -55,6 +58,7 @@ export class AppService { } async init() { + await this.configService.init(); this.storageService.init(); await this.serverService.handleVersionCheck(); this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`); diff --git a/server/src/immich/interceptors/error.interceptor.ts b/server/src/immich/interceptors/error.interceptor.ts index 9ccdabd72f..f3f944bdb4 100644 --- a/server/src/immich/interceptors/error.interceptor.ts +++ b/server/src/immich/interceptors/error.interceptor.ts @@ -1,10 +1,10 @@ +import { ImmichLogger } from '@app/infra/logger'; import { CallHandler, ExecutionContext, HttpException, Injectable, InternalServerErrorException, - Logger, NestInterceptor, } from '@nestjs/common'; import { Observable, catchError, throwError } from 'rxjs'; @@ -13,7 +13,7 @@ import { routeToErrorMessage } from '../app.utils'; @Injectable() export class ErrorInterceptor implements NestInterceptor { - private logger = new Logger(ErrorInterceptor.name); + private logger = new ImmichLogger(ErrorInterceptor.name); async intercept(context: ExecutionContext, next: CallHandler): Promise> { return next.handle().pipe( diff --git a/server/src/immich/interceptors/file-serve.interceptor.ts b/server/src/immich/interceptors/file-serve.interceptor.ts index 39e9aa4d64..e4528dc305 100644 --- a/server/src/immich/interceptors/file-serve.interceptor.ts +++ b/server/src/immich/interceptors/file-serve.interceptor.ts @@ -1,5 +1,6 @@ import { ImmichFileResponse, isConnectionAborted } from '@app/domain'; -import { CallHandler, ExecutionContext, Logger, NestInterceptor } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; import { Response } from 'express'; import { access, constants } from 'fs/promises'; import { isAbsolute } from 'path'; @@ -10,7 +11,7 @@ type SendFile = Parameters; type SendFileOptions = SendFile[1]; export class FileServeInterceptor implements NestInterceptor { - private logger = new Logger(FileServeInterceptor.name); + private logger = new ImmichLogger(FileServeInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable { const http = context.switchToHttp(); diff --git a/server/src/immich/interceptors/file-upload.interceptor.ts b/server/src/immich/interceptors/file-upload.interceptor.ts index 0fb59014d9..9cd7620778 100644 --- a/server/src/immich/interceptors/file-upload.interceptor.ts +++ b/server/src/immich/interceptors/file-upload.interceptor.ts @@ -1,5 +1,6 @@ import { AssetService, UploadFieldName, UploadFile } from '@app/domain'; -import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { PATH_METADATA } from '@nestjs/common/constants'; import { Reflector } from '@nestjs/core'; import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; @@ -52,7 +53,7 @@ const asRequest = (req: AuthRequest, file: Express.Multer.File) => { @Injectable() export class FileUploadInterceptor implements NestInterceptor { - private logger = new Logger(FileUploadInterceptor.name); + private logger = new ImmichLogger(FileUploadInterceptor.name); private handlers: { userProfile: RequestHandler; diff --git a/server/src/immich/main.ts b/server/src/immich/main.ts index afc3a41c6d..84bc5bd1c2 100644 --- a/server/src/immich/main.ts +++ b/server/src/immich/main.ts @@ -1,6 +1,6 @@ -import { envName, getLogLevels, isDev, serverVersion } from '@app/domain'; +import { envName, isDev, serverVersion } from '@app/domain'; import { WebSocketAdapter, enablePrefilter } from '@app/infra'; -import { Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { json } from 'body-parser'; @@ -9,12 +9,13 @@ import { AppModule } from './app.module'; import { AppService } from './app.service'; import { useSwagger } from './app.utils'; -const logger = new Logger('ImmichServer'); +const logger = new ImmichLogger('ImmichServer'); const port = Number(process.env.SERVER_PORT) || 3001; export async function bootstrap() { - const app = await NestFactory.create(AppModule, { logger: getLogLevels() }); + const app = await NestFactory.create(AppModule, { bufferLogs: true }); + app.useLogger(app.get(ImmichLogger)); app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']); app.set('etag', 'strong'); app.use(cookieParser()); diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index f6c14e1a7d..b5b5930548 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -45,6 +45,12 @@ export enum SystemConfigKey { JOB_LIBRARY_CONCURRENCY = 'job.library.concurrency', JOB_MIGRATION_CONCURRENCY = 'job.migration.concurrency', + LIBRARY_SCAN_ENABLED = 'library.scan.enabled', + LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression', + + LOGGING_ENABLED = 'logging.enabled', + LOGGING_LEVEL = 'logging.level', + MACHINE_LEARNING_ENABLED = 'machineLearning.enabled', MACHINE_LEARNING_URL = 'machineLearning.url', @@ -94,9 +100,6 @@ export enum SystemConfigKey { TRASH_DAYS = 'trash.days', THEME_CUSTOM_CSS = 'theme.customCss', - - LIBRARY_SCAN_ENABLED = 'library.scan.enabled', - LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression', } export enum TranscodePolicy { @@ -144,6 +147,15 @@ export enum Colorspace { P3 = 'p3', } +export enum LogLevel { + VERBOSE = 'verbose', + DEBUG = 'debug', + LOG = 'log', + WARN = 'warn', + ERROR = 'error', + FATAL = 'fatal', +} + export interface SystemConfig { ffmpeg: { crf: number; @@ -165,6 +177,10 @@ export interface SystemConfig { tonemap: ToneMapping; }; job: Record; + logging: { + enabled: boolean; + level: LogLevel; + }; machineLearning: { enabled: boolean; url: string; diff --git a/server/src/infra/logger.ts b/server/src/infra/logger.ts new file mode 100644 index 0000000000..c059111d21 --- /dev/null +++ b/server/src/infra/logger.ts @@ -0,0 +1,21 @@ +import { ConsoleLogger } from '@nestjs/common'; +import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; +import { LogLevel } from './entities'; + +const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; + +export class ImmichLogger extends ConsoleLogger { + private static logLevels: LogLevel[] = []; + + constructor(context: string) { + super(context); + } + + isLevelEnabled(level: LogLevel) { + return isLogLevelEnabled(level, ImmichLogger.logLevels); + } + + static setLogLevel(level: LogLevel | false): void { + ImmichLogger.logLevels = level === false ? [] : LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)); + } +} diff --git a/server/src/infra/repositories/communication.repository.ts b/server/src/infra/repositories/communication.repository.ts index 558c911c2b..160a595298 100644 --- a/server/src/infra/repositories/communication.repository.ts +++ b/server/src/infra/repositories/communication.repository.ts @@ -6,7 +6,7 @@ import { OnServerEventCallback, ServerEvent, } from '@app/domain'; -import { Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; import { OnGatewayConnection, OnGatewayDisconnect, @@ -20,7 +20,7 @@ import { Server, Socket } from 'socket.io'; export class CommunicationRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, ICommunicationRepository { - private logger = new Logger(CommunicationRepository.name); + private logger = new ImmichLogger(CommunicationRepository.name); private onConnectCallbacks: OnConnectCallback[] = []; private onServerEventCallbacks: Record = { [ServerEvent.CONFIG_UPDATE]: [], diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/infra/repositories/filesystem.provider.ts index 7edcede806..417f80a109 100644 --- a/server/src/infra/repositories/filesystem.provider.ts +++ b/server/src/infra/repositories/filesystem.provider.ts @@ -6,7 +6,7 @@ import { IStorageRepository, mimeTypes, } from '@app/domain'; -import { Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; import archiver from 'archiver'; import { constants, createReadStream, existsSync, mkdirSync } from 'fs'; import fs, { readdir, writeFile } from 'fs/promises'; @@ -18,7 +18,7 @@ import path from 'path'; const moveFile = promisify(mv); export class FilesystemProvider implements IStorageRepository { - private logger = new Logger(FilesystemProvider.name); + private logger = new ImmichLogger(FilesystemProvider.name); createZipStream(): ImmichZipStream { const archive = archiver('zip', { store: true }); diff --git a/server/src/infra/repositories/job.repository.ts b/server/src/infra/repositories/job.repository.ts index a359845fcf..61238fac78 100644 --- a/server/src/infra/repositories/job.repository.ts +++ b/server/src/infra/repositories/job.repository.ts @@ -8,8 +8,9 @@ import { QueueName, QueueStatus, } from '@app/domain'; +import { ImmichLogger } from '@app/infra/logger'; import { getQueueToken } from '@nestjs/bullmq'; -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq'; @@ -19,7 +20,7 @@ import { bullConfig } from '../infra.config'; @Injectable() export class JobRepository implements IJobRepository { private workers: Partial> = {}; - private logger = new Logger(JobRepository.name); + private logger = new ImmichLogger(JobRepository.name); constructor( private moduleRef: ModuleRef, diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index 519094418c..640b891a76 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/infra/repositories/media.repository.ts @@ -1,6 +1,6 @@ import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoInfo } from '@app/domain'; import { Colorspace } from '@app/infra/entities'; -import { Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; import fs from 'fs/promises'; import sharp from 'sharp'; @@ -11,7 +11,7 @@ const probe = promisify(ffmpeg.ffprobe); sharp.concurrency(0); export class MediaRepository implements IMediaRepository { - private logger = new Logger(MediaRepository.name); + private logger = new ImmichLogger(MediaRepository.name); crop(input: string | Buffer, options: CropOptions): Promise { return sharp(input, { failOn: 'none' }) diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 15ccabd0c4..f573eb456a 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -7,7 +7,8 @@ import { } from '@app/domain'; import { DatabaseLock, RequireLock } from '@app/infra'; import { GeodataAdmin1Entity, GeodataAdmin2Entity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities'; -import { Inject, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Inject } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored'; import { createReadStream, existsSync } from 'fs'; @@ -31,7 +32,7 @@ export class MetadataRepository implements IMetadataRepository { @InjectDataSource() private dataSource: DataSource, ) {} - private logger = new Logger(MetadataRepository.name); + private logger = new ImmichLogger(MetadataRepository.name); @RequireLock(DatabaseLock.GeodataImport) async init(): Promise { diff --git a/server/src/infra/repositories/smart-info.repository.ts b/server/src/infra/repositories/smart-info.repository.ts index 58d7e4c1e1..57aaa4d3c7 100644 --- a/server/src/infra/repositories/smart-info.repository.ts +++ b/server/src/infra/repositories/smart-info.repository.ts @@ -2,7 +2,8 @@ import { Embedding, EmbeddingSearch, ISmartInfoRepository } from '@app/domain'; import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; import { DatabaseLock, RequireLock, asyncLock } from '@app/infra'; import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities'; -import { Injectable, Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { DummyValue, GenerateSql } from '../infra.util'; @@ -10,7 +11,7 @@ import { asVector, isValidInteger } from '../infra.utils'; @Injectable() export class SmartInfoRepository implements ISmartInfoRepository { - private logger = new Logger(SmartInfoRepository.name); + private logger = new ImmichLogger(SmartInfoRepository.name); private faceColumns: string[]; constructor( diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index abbd8a6bde..864215e706 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -8,37 +8,33 @@ import { MediaService, MetadataService, PersonService, - ServerInfoService, SmartInfoService, StorageService, StorageTemplateService, SystemConfigService, UserService, } from '@app/domain'; - -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - private logger = new Logger(AppService.name); - constructor( private auditService: AuditService, private assetService: AssetService, + private configService: SystemConfigService, private jobService: JobService, private libraryService: LibraryService, private mediaService: MediaService, private metadataService: MetadataService, private personService: PersonService, - private serverInfoService: ServerInfoService, private smartInfoService: SmartInfoService, private storageTemplateService: StorageTemplateService, private storageService: StorageService, - private systemConfigService: SystemConfigService, private userService: UserService, ) {} async init() { + await this.configService.init(); await this.jobService.registerHandlers({ [JobName.ASSET_DELETION]: (data) => this.assetService.handleAssetDeletion(data), [JobName.ASSET_DELETION_CHECK]: () => this.assetService.handleAssetDeletionCheck(), diff --git a/server/src/microservices/main.ts b/server/src/microservices/main.ts index 1d371bb9c2..c7e0662800 100644 --- a/server/src/microservices/main.ts +++ b/server/src/microservices/main.ts @@ -1,20 +1,19 @@ -import { envName, getLogLevels, serverVersion } from '@app/domain'; +import { envName, serverVersion } from '@app/domain'; import { WebSocketAdapter, enablePrefilter } from '@app/infra'; -import { Logger } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; import { NestFactory } from '@nestjs/core'; -import { AppService } from './app.service'; import { MicroservicesModule } from './microservices.module'; -const logger = new Logger('ImmichMicroservice'); +const logger = new ImmichLogger('ImmichMicroservice'); const port = Number(process.env.MICROSERVICES_PORT) || 3002; export async function bootstrap() { - const app = await NestFactory.create(MicroservicesModule, { logger: getLogLevels() }); + const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true }); + app.useLogger(app.get(ImmichLogger)); app.useWebSocketAdapter(new WebSocketAdapter(app)); await enablePrefilter(); - await app.get(AppService).init(); await app.listen(port); logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `); diff --git a/server/src/microservices/microservices.module.ts b/server/src/microservices/microservices.module.ts index bcbf48d9af..9ffe4e38a9 100644 --- a/server/src/microservices/microservices.module.ts +++ b/server/src/microservices/microservices.module.ts @@ -1,10 +1,16 @@ import { DomainModule } from '@app/domain'; import { InfraModule } from '@app/infra'; -import { Module } from '@nestjs/common'; +import { Module, OnModuleInit } from '@nestjs/common'; import { AppService } from './app.service'; @Module({ imports: [DomainModule.register({ imports: [InfraModule] })], providers: [AppService], }) -export class MicroservicesModule {} +export class MicroservicesModule implements OnModuleInit { + constructor(private appService: AppService) {} + + async onModuleInit() { + await this.appService.init(); + } +} diff --git a/server/test/setup.ts b/server/test/setup.ts deleted file mode 100644 index 0a2bd92b64..0000000000 --- a/server/test/setup.ts +++ /dev/null @@ -1,11 +0,0 @@ -jest.mock('@nestjs/common', () => ({ - ...jest.requireActual('@nestjs/common'), - Logger: jest.fn().mockReturnValue({ - verbose: jest.fn(), - debug: jest.fn(), - log: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }), -})); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index e74361de77..965030af4b 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -2175,6 +2175,24 @@ export const LibraryType = { export type LibraryType = typeof LibraryType[keyof typeof LibraryType]; +/** + * + * @export + * @enum {string} + */ + +export const LogLevel = { + Verbose: 'verbose', + Debug: 'debug', + Log: 'log', + Warn: 'warn', + Error: 'error', + Fatal: 'fatal' +} as const; + +export type LogLevel = typeof LogLevel[keyof typeof LogLevel]; + + /** * * @export @@ -3577,6 +3595,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'library': SystemConfigLibraryDto; + /** + * + * @type {SystemConfigLoggingDto} + * @memberof SystemConfigDto + */ + 'logging': SystemConfigLoggingDto; /** * * @type {SystemConfigMachineLearningDto} @@ -3860,6 +3884,27 @@ export interface SystemConfigLibraryScanDto { */ 'enabled': boolean; } +/** + * + * @export + * @interface SystemConfigLoggingDto + */ +export interface SystemConfigLoggingDto { + /** + * + * @type {boolean} + * @memberof SystemConfigLoggingDto + */ + 'enabled': boolean; + /** + * + * @type {LogLevel} + * @memberof SystemConfigLoggingDto + */ + 'level': LogLevel; +} + + /** * * @export diff --git a/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte b/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte new file mode 100644 index 0000000000..70fa2dc8d1 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte @@ -0,0 +1,110 @@ + + +
+ {#await getConfigs() then} +
+
+
+
+ +
+ +
+ + + +
+
+
+
+ {/await} +
diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 5a95b0b920..8f3cc9f5fd 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -21,6 +21,7 @@ import type { PageData } from './$types'; import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte'; import LibrarySettings from '$lib/components/admin-page/settings/library-settings/library-settings.svelte'; + import LoggingSettings from '$lib/components/admin-page/settings/logging-settings/logging-settings.svelte'; import { mdiAlert, mdiContentCopy, mdiDownload } from '@mdi/js'; export let data: PageData; @@ -74,6 +75,10 @@ + + + +