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
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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<void>;
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
export const defaults = Object.freeze<SystemConfig>({
|
||||
ffmpeg: {
|
||||
@@ -57,6 +59,10 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
[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<SystemConfigValue>[] | null = null;
|
||||
|
||||
@@ -253,14 +259,16 @@ export class SystemConfigCore {
|
||||
return config;
|
||||
}
|
||||
|
||||
public async updateConfig(config: SystemConfig): Promise<SystemConfig> {
|
||||
public async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
|
||||
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() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
CQMode,
|
||||
LogLevel,
|
||||
SystemConfig,
|
||||
SystemConfigEntity,
|
||||
SystemConfigKey,
|
||||
@@ -57,6 +58,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user