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 e69cfce6ee..6f39840261 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -281,6 +281,7 @@ Class | Method | HTTP request | Description - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LibraryType](doc//LibraryType.md) + - [LogLevel](doc//LogLevel.md) - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) @@ -334,6 +335,7 @@ Class | Method | HTTP request | Description - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) - [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md) + - [SystemConfigLoggingDto](doc//SystemConfigLoggingDto.md) - [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md) - [SystemConfigMapDto](doc//SystemConfigMapDto.md) - [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md) diff --git a/mobile/openapi/doc/LogLevel.md b/mobile/openapi/doc/LogLevel.md new file mode 100644 index 0000000000..84b40e5d80 --- /dev/null +++ b/mobile/openapi/doc/LogLevel.md @@ -0,0 +1,14 @@ +# openapi.model.LogLevel + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index 73c5b70dcf..403260659a 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -11,6 +11,7 @@ Name | Type | Description | Notes **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | | **library_** | [**SystemConfigLibraryDto**](SystemConfigLibraryDto.md) | | +**logging** | [**SystemConfigLoggingDto**](SystemConfigLoggingDto.md) | | **machineLearning** | [**SystemConfigMachineLearningDto**](SystemConfigMachineLearningDto.md) | | **map** | [**SystemConfigMapDto**](SystemConfigMapDto.md) | | **newVersionCheck** | [**SystemConfigNewVersionCheckDto**](SystemConfigNewVersionCheckDto.md) | | diff --git a/mobile/openapi/doc/SystemConfigLoggingDto.md b/mobile/openapi/doc/SystemConfigLoggingDto.md new file mode 100644 index 0000000000..d63f4548d3 --- /dev/null +++ b/mobile/openapi/doc/SystemConfigLoggingDto.md @@ -0,0 +1,16 @@ +# openapi.model.SystemConfigLoggingDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**enabled** | **bool** | | +**level** | [**LogLevel**](LogLevel.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index c0caf20e4e..b447a5e681 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -117,6 +117,7 @@ part 'model/job_status_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; part 'model/library_type.dart'; +part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; @@ -170,6 +171,7 @@ part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; part 'model/system_config_library_scan_dto.dart'; +part 'model/system_config_logging_dto.dart'; part 'model/system_config_machine_learning_dto.dart'; part 'model/system_config_map_dto.dart'; part 'model/system_config_new_version_check_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7d376949e7..ae5848b06b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -321,6 +321,8 @@ class ApiClient { return LibraryStatsResponseDto.fromJson(value); case 'LibraryType': return LibraryTypeTypeTransformer().decode(value); + case 'LogLevel': + return LogLevelTypeTransformer().decode(value); case 'LoginCredentialDto': return LoginCredentialDto.fromJson(value); case 'LoginResponseDto': @@ -427,6 +429,8 @@ class ApiClient { return SystemConfigLibraryDto.fromJson(value); case 'SystemConfigLibraryScanDto': return SystemConfigLibraryScanDto.fromJson(value); + case 'SystemConfigLoggingDto': + return SystemConfigLoggingDto.fromJson(value); case 'SystemConfigMachineLearningDto': return SystemConfigMachineLearningDto.fromJson(value); case 'SystemConfigMapDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 728a4ed833..bab5b9c6fa 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -88,6 +88,9 @@ String parameterToString(dynamic value) { if (value is LibraryType) { return LibraryTypeTypeTransformer().encode(value).toString(); } + if (value is LogLevel) { + return LogLevelTypeTransformer().encode(value).toString(); + } if (value is MapTheme) { return MapThemeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/log_level.dart b/mobile/openapi/lib/model/log_level.dart new file mode 100644 index 0000000000..a5c50eb40c --- /dev/null +++ b/mobile/openapi/lib/model/log_level.dart @@ -0,0 +1,97 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class LogLevel { + /// Instantiate a new enum with the provided [value]. + const LogLevel._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const verbose = LogLevel._(r'verbose'); + static const debug = LogLevel._(r'debug'); + static const log = LogLevel._(r'log'); + static const warn = LogLevel._(r'warn'); + static const error = LogLevel._(r'error'); + static const fatal = LogLevel._(r'fatal'); + + /// List of all possible values in this [enum][LogLevel]. + static const values = [ + verbose, + debug, + log, + warn, + error, + fatal, + ]; + + static LogLevel? fromJson(dynamic value) => LogLevelTypeTransformer().decode(value); + + static List? listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = LogLevel.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [LogLevel] to String, +/// and [decode] dynamic data back to [LogLevel]. +class LogLevelTypeTransformer { + factory LogLevelTypeTransformer() => _instance ??= const LogLevelTypeTransformer._(); + + const LogLevelTypeTransformer._(); + + String encode(LogLevel data) => data.value; + + /// Decodes a [dynamic value][data] to a LogLevel. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + LogLevel? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'verbose': return LogLevel.verbose; + case r'debug': return LogLevel.debug; + case r'log': return LogLevel.log; + case r'warn': return LogLevel.warn; + case r'error': return LogLevel.error; + case r'fatal': return LogLevel.fatal; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [LogLevelTypeTransformer] instance. + static LogLevelTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index c8407c2ce2..e7214c201a 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -16,6 +16,7 @@ class SystemConfigDto { required this.ffmpeg, required this.job, required this.library_, + required this.logging, required this.machineLearning, required this.map, required this.newVersionCheck, @@ -34,6 +35,8 @@ class SystemConfigDto { SystemConfigLibraryDto library_; + SystemConfigLoggingDto logging; + SystemConfigMachineLearningDto machineLearning; SystemConfigMapDto map; @@ -59,6 +62,7 @@ class SystemConfigDto { other.ffmpeg == ffmpeg && other.job == job && other.library_ == library_ && + other.logging == logging && other.machineLearning == machineLearning && other.map == map && other.newVersionCheck == newVersionCheck && @@ -76,6 +80,7 @@ class SystemConfigDto { (ffmpeg.hashCode) + (job.hashCode) + (library_.hashCode) + + (logging.hashCode) + (machineLearning.hashCode) + (map.hashCode) + (newVersionCheck.hashCode) + @@ -88,13 +93,14 @@ class SystemConfigDto { (trash.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash]'; Map toJson() { final json = {}; json[r'ffmpeg'] = this.ffmpeg; json[r'job'] = this.job; json[r'library'] = this.library_; + json[r'logging'] = this.logging; json[r'machineLearning'] = this.machineLearning; json[r'map'] = this.map; json[r'newVersionCheck'] = this.newVersionCheck; @@ -119,6 +125,7 @@ class SystemConfigDto { ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, job: SystemConfigJobDto.fromJson(json[r'job'])!, library_: SystemConfigLibraryDto.fromJson(json[r'library'])!, + logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!, machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!, map: SystemConfigMapDto.fromJson(json[r'map'])!, newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!, @@ -179,6 +186,7 @@ class SystemConfigDto { 'ffmpeg', 'job', 'library', + 'logging', 'machineLearning', 'map', 'newVersionCheck', diff --git a/mobile/openapi/lib/model/system_config_logging_dto.dart b/mobile/openapi/lib/model/system_config_logging_dto.dart new file mode 100644 index 0000000000..ce2c75c221 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_logging_dto.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigLoggingDto { + /// Returns a new [SystemConfigLoggingDto] instance. + SystemConfigLoggingDto({ + required this.enabled, + required this.level, + }); + + bool enabled; + + LogLevel level; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigLoggingDto && + other.enabled == enabled && + other.level == level; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (level.hashCode); + + @override + String toString() => 'SystemConfigLoggingDto[enabled=$enabled, level=$level]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'level'] = this.level; + return json; + } + + /// Returns a new [SystemConfigLoggingDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigLoggingDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigLoggingDto( + enabled: mapValueOfType(json, r'enabled')!, + level: LogLevel.fromJson(json[r'level'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigLoggingDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigLoggingDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigLoggingDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigLoggingDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'level', + }; +} + diff --git a/mobile/openapi/test/log_level_test.dart b/mobile/openapi/test/log_level_test.dart new file mode 100644 index 0000000000..dfe841bf07 --- /dev/null +++ b/mobile/openapi/test/log_level_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for LogLevel +void main() { + + group('test LogLevel', () { + + }); + +} diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index c8b5c0d9c1..5398f77601 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -31,6 +31,11 @@ void main() { // TODO }); + // SystemConfigLoggingDto logging + test('to test the property `logging`', () async { + // TODO + }); + // SystemConfigMachineLearningDto machineLearning test('to test the property `machineLearning`', () async { // TODO 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 0000000000..cc638f5310 --- /dev/null +++ b/mobile/openapi/test/system_config_logging_dto_test.dart @@ -0,0 +1,32 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for SystemConfigLoggingDto +void main() { + // final instance = SystemConfigLoggingDto(); + + group('test SystemConfigLoggingDto', () { + // bool enabled + test('to test the property `enabled`', () async { + // TODO + }); + + // LogLevel level + test('to test the property `level`', () async { + // TODO + }); + + + }); + +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 8007e91a87..82fc574444 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -7949,6 +7949,17 @@ ], "type": "string" }, + "LogLevel": { + "enum": [ + "verbose", + "debug", + "log", + "warn", + "error", + "fatal" + ], + "type": "string" + }, "LoginCredentialDto": { "properties": { "email": { @@ -9039,6 +9050,9 @@ "library": { "$ref": "#/components/schemas/SystemConfigLibraryDto" }, + "logging": { + "$ref": "#/components/schemas/SystemConfigLoggingDto" + }, "machineLearning": { "$ref": "#/components/schemas/SystemConfigMachineLearningDto" }, @@ -9072,6 +9086,7 @@ }, "required": [ "ffmpeg", + "logging", "machineLearning", "map", "newVersionCheck", @@ -9243,6 +9258,21 @@ ], "type": "object" }, + "SystemConfigLoggingDto": { + "properties": { + "enabled": { + "type": "boolean" + }, + "level": { + "$ref": "#/components/schemas/LogLevel" + } + }, + "required": [ + "level", + "enabled" + ], + "type": "object" + }, "SystemConfigMachineLearningDto": { "properties": { "classification": { diff --git a/server/package.json b/server/package.json index f188394614..3ae01fe673 100644 --- a/server/package.json +++ b/server/package.json @@ -153,9 +153,6 @@ "statements": 90 } }, - "setupFilesAfterEnv": [ - "/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 @@ + + + +