settings for metrics
This commit is contained in:
@@ -1,17 +1,59 @@
|
||||
export class MetricsServerInfoDto {
|
||||
cpuCount?: number;
|
||||
cpuModel?: string;
|
||||
memoryCount?: number;
|
||||
version?: string;
|
||||
import { IsBoolean } from 'class-validator';
|
||||
|
||||
// TODO I feel like it must be possible to generate those from MetricsServerInfo and MetricsAssetCount
|
||||
export class MetricServerInfoConfig {
|
||||
@IsBoolean()
|
||||
cpuCount!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
cpuModel!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
memory!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
version!: boolean;
|
||||
}
|
||||
|
||||
export class MetricsAssetCountDto {
|
||||
image?: number;
|
||||
video?: number;
|
||||
total?: number;
|
||||
export class MetricsAssetCountConfig {
|
||||
@IsBoolean()
|
||||
image!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
video!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
total!: boolean;
|
||||
}
|
||||
|
||||
export class MetricsDto {
|
||||
serverInfo!: MetricsServerInfoDto;
|
||||
assetCount!: MetricsAssetCountDto;
|
||||
class MetricsServerInfo {
|
||||
cpuCount!: number;
|
||||
cpuModel!: string;
|
||||
memory!: number;
|
||||
version!: string;
|
||||
}
|
||||
|
||||
class MetricsAssetCount {
|
||||
image!: number;
|
||||
video!: number;
|
||||
total!: number;
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
serverInfo: {
|
||||
cpuCount: number;
|
||||
cpuModel: string;
|
||||
memory: number;
|
||||
version: string;
|
||||
};
|
||||
assetCount: {
|
||||
image: number;
|
||||
video: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class MetricsDto implements Metrics {
|
||||
serverInfo!: MetricsServerInfo;
|
||||
assetCount!: MetricsAssetCount;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,71 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { serverVersion } from '../domain.constant';
|
||||
import { JobName } from '../job';
|
||||
import { ISystemConfigRepository } from '../repositories';
|
||||
import { IJobRepository } from '../repositories/job.repository';
|
||||
import { IMetricsRepository, SharedMetrics } from '../repositories/metrics.repository';
|
||||
import { MetricsDto } from './metrics.dto';
|
||||
import { IMetricsRepository } from '../repositories/metrics.repository';
|
||||
import { FeatureFlag, SystemConfigCore, SystemConfigMetricsDto } from '../system-config';
|
||||
import { Metrics, MetricsDto } from './metrics.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MetricsService {
|
||||
private configCore: SystemConfigCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IMetricsRepository) private repository: IMetricsRepository,
|
||||
) {}
|
||||
@Inject(ISystemConfigRepository) systemConfigRepository: ISystemConfigRepository,
|
||||
) {
|
||||
this.configCore = SystemConfigCore.create(systemConfigRepository);
|
||||
}
|
||||
|
||||
async handleQueueMetrics() {
|
||||
// TODO config for what metrics should be fetched and if any at all
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.METRICS, data: { assetCount: true, serverInfo: true } });
|
||||
if (await this.configCore.hasFeature(FeatureFlag.METRICS)) {
|
||||
await this.jobRepository.queue({ name: JobName.METRICS });
|
||||
}
|
||||
}
|
||||
|
||||
async handleMetrics(metrics: SharedMetrics) {
|
||||
const metricsPayload = new MetricsDto();
|
||||
if (metrics.serverInfo) {
|
||||
metricsPayload.serverInfo.version = serverVersion.toString();
|
||||
metricsPayload.serverInfo.cpuCount = this.repository.getCpuCount();
|
||||
metricsPayload.serverInfo.cpuModel = this.repository.getCpuModel();
|
||||
metricsPayload.serverInfo.memoryCount = this.repository.getMemoryCount();
|
||||
}
|
||||
async handleSendMetrics() {
|
||||
const metricsConfig = await this.configCore.getConfig().then((config) => config.metrics);
|
||||
const metrics = await this.getMetrics(metricsConfig);
|
||||
|
||||
if (metrics.assetCount) {
|
||||
metricsPayload.assetCount.image = await this.repository.getImageCount();
|
||||
metricsPayload.assetCount.video = await this.repository.getVideoCount();
|
||||
metricsPayload.assetCount.total = await this.repository.getAssetCount();
|
||||
}
|
||||
|
||||
await this.repository.sendMetrics(metricsPayload);
|
||||
await this.repository.sendMetrics(metrics);
|
||||
return true;
|
||||
}
|
||||
|
||||
async getMetrics(config: SystemConfigMetricsDto) {
|
||||
const metrics: Metrics = new MetricsDto();
|
||||
|
||||
metrics.serverInfo = {
|
||||
cpuCount: this.repository.getCpuCount(),
|
||||
cpuModel: this.repository.getCpuModel(),
|
||||
memory: this.repository.getMemory(),
|
||||
version: serverVersion.toString(),
|
||||
};
|
||||
|
||||
metrics.assetCount = {
|
||||
image: await this.repository.getImageCount(),
|
||||
video: await this.repository.getVideoCount(),
|
||||
total: await this.repository.getAssetCount(),
|
||||
};
|
||||
|
||||
return _.pick(metrics, this.getKeys(config));
|
||||
}
|
||||
|
||||
private getKeys(config: SystemConfigMetricsDto) {
|
||||
const result = [];
|
||||
const keys = _.keys(config) as Array<keyof SystemConfigMetricsDto>;
|
||||
for (const key of keys) {
|
||||
const subConfig = _.get(config, key);
|
||||
if (typeof subConfig === 'boolean') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keys = _.keys(_.pickBy(subConfig)).map((value) => `${key}.${value}`);
|
||||
result.push(...keys);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ILibraryRefreshJob,
|
||||
ISidecarWriteJob,
|
||||
} from '../job/job.interface';
|
||||
import { SharedMetrics } from './metrics.repository';
|
||||
|
||||
export interface JobCounts {
|
||||
active: number;
|
||||
@@ -93,7 +92,7 @@ export type JobItem =
|
||||
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
||||
|
||||
// Metrics
|
||||
| { name: JobName.METRICS; data: SharedMetrics };
|
||||
| { name: JobName.METRICS; data?: IBaseJob };
|
||||
|
||||
export type JobHandler<T = any> = (data: T) => boolean | Promise<boolean>;
|
||||
export type JobItemHandler = (item: JobItem) => Promise<void>;
|
||||
|
||||
@@ -2,17 +2,12 @@ import { MetricsDto } from '../metrics';
|
||||
|
||||
export const IMetricsRepository = 'IMetricsRepository';
|
||||
|
||||
export interface SharedMetrics {
|
||||
serverInfo: boolean;
|
||||
assetCount: boolean;
|
||||
}
|
||||
|
||||
export interface IMetricsRepository {
|
||||
getAssetCount(): Promise<number>;
|
||||
getCpuCount(): number;
|
||||
getCpuModel(): string;
|
||||
getMemoryCount(): number;
|
||||
getMemory(): number;
|
||||
getImageCount(): Promise<number>;
|
||||
getVideoCount(): Promise<number>;
|
||||
sendMetrics(payload: MetricsDto): Promise<void>;
|
||||
sendMetrics(payload: Partial<MetricsDto>): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ export class ServerFeaturesDto implements FeatureFlags {
|
||||
configFile!: boolean;
|
||||
facialRecognition!: boolean;
|
||||
map!: boolean;
|
||||
metrics!: boolean;
|
||||
trash!: boolean;
|
||||
reverseGeocoding!: boolean;
|
||||
oauth!: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './system-config-ffmpeg.dto';
|
||||
export * from './system-config-library.dto';
|
||||
export * from './system-config-metrics.dto';
|
||||
export * from './system-config-oauth.dto';
|
||||
export * from './system-config-password-login.dto';
|
||||
export * from './system-config-storage-template.dto';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MetricServerInfoConfig, MetricsAssetCountConfig } from '@app/domain';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsBoolean, IsObject, ValidateNested } from 'class-validator';
|
||||
|
||||
export class SystemConfigMetricsDto {
|
||||
@IsBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@Type(() => MetricServerInfoConfig)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
serverInfo!: MetricServerInfoConfig;
|
||||
|
||||
@Type(() => MetricsAssetCountConfig)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
assetCount!: MetricsAssetCountConfig;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SystemConfig } from '@app/infra/entities';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsObject, ValidateNested } from 'class-validator';
|
||||
import { SystemConfigMetricsDto } from '.';
|
||||
import { SystemConfigFFmpegDto } from './system-config-ffmpeg.dto';
|
||||
import { SystemConfigJobDto } from './system-config-job.dto';
|
||||
import { SystemConfigLibraryDto } from './system-config-library.dto';
|
||||
@@ -37,6 +38,11 @@ export class SystemConfigDto implements SystemConfig {
|
||||
@IsObject()
|
||||
map!: SystemConfigMapDto;
|
||||
|
||||
@Type(() => SystemConfigMetricsDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
metrics!: SystemConfigMetricsDto;
|
||||
|
||||
@Type(() => SystemConfigNewVersionCheckDto)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
||||
@@ -82,6 +82,20 @@ export const defaults = Object.freeze<SystemConfig>({
|
||||
lightStyle: '',
|
||||
darkStyle: '',
|
||||
},
|
||||
metrics: {
|
||||
enabled: false,
|
||||
serverInfo: {
|
||||
cpuCount: true,
|
||||
cpuModel: true,
|
||||
memory: true,
|
||||
version: true,
|
||||
},
|
||||
assetCount: {
|
||||
image: true,
|
||||
video: true,
|
||||
total: true,
|
||||
},
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: true,
|
||||
},
|
||||
@@ -132,6 +146,7 @@ export enum FeatureFlag {
|
||||
CLIP_ENCODE = 'clipEncode',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
MAP = 'map',
|
||||
METRICS = 'metrics',
|
||||
REVERSE_GEOCODING = 'reverseGeocoding',
|
||||
SIDECAR = 'sidecar',
|
||||
SEARCH = 'search',
|
||||
@@ -204,6 +219,7 @@ export class SystemConfigCore {
|
||||
[FeatureFlag.CLIP_ENCODE]: mlEnabled && config.machineLearning.clip.enabled,
|
||||
[FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
|
||||
[FeatureFlag.MAP]: config.map.enabled,
|
||||
[FeatureFlag.METRICS]: config.metrics.enabled,
|
||||
[FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled,
|
||||
[FeatureFlag.SIDECAR]: true,
|
||||
[FeatureFlag.SEARCH]: true,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
MetricsController,
|
||||
OAuthController,
|
||||
PartnerController,
|
||||
PersonController,
|
||||
@@ -54,6 +55,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
MetricsController,
|
||||
OAuthController,
|
||||
PartnerController,
|
||||
SearchController,
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './auth.controller';
|
||||
export * from './face.controller';
|
||||
export * from './job.controller';
|
||||
export * from './library.controller';
|
||||
export * from './metrics.controller';
|
||||
export * from './oauth.controller';
|
||||
export * from './partner.controller';
|
||||
export * from './person.controller';
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Metrics, MetricsService, SystemConfigMetricsDto } from '@app/domain';
|
||||
import { Body, Controller, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
|
||||
@ApiTags('Metrics')
|
||||
@Controller('metrics')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class MetricsController {
|
||||
constructor(private service: MetricsService) {}
|
||||
|
||||
@Put()
|
||||
getMetrics(@Body() dto: SystemConfigMetricsDto): Promise<Partial<Metrics>> {
|
||||
return this.service.getMetrics(dto);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,15 @@ export enum SystemConfigKey {
|
||||
MAP_LIGHT_STYLE = 'map.lightStyle',
|
||||
MAP_DARK_STYLE = 'map.darkStyle',
|
||||
|
||||
METRICS_ENABLED = 'metrics.enabled',
|
||||
METRICS_SERVER_INFO_CPU_COUNT = 'metrics.serverInfo.cpuCount',
|
||||
METRICS_SERVER_INFO_CPU_MODEL = 'metrics.serverInfo.cpuModel',
|
||||
METRICS_SERVER_INFO_MEMORY = 'metrics.serverInfo.memory',
|
||||
METRICS_SERVER_INFO_VERSION = 'metrics.serverInfo.version',
|
||||
METRICS_ASSET_COUNT_IMAGE = 'metrics.assetCount.image',
|
||||
METRICS_ASSET_COUNT_VIDEO = 'metrics.assetCount.video',
|
||||
METRICS_ASSET_COUNT_TOTAL = 'metrics.assetCount.total',
|
||||
|
||||
REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled',
|
||||
|
||||
NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled',
|
||||
@@ -196,6 +205,20 @@ export interface SystemConfig {
|
||||
lightStyle: string;
|
||||
darkStyle: string;
|
||||
};
|
||||
metrics: {
|
||||
enabled: boolean;
|
||||
serverInfo: {
|
||||
cpuCount: boolean;
|
||||
cpuModel: boolean;
|
||||
memory: boolean;
|
||||
version: boolean;
|
||||
};
|
||||
assetCount: {
|
||||
image: boolean;
|
||||
video: boolean;
|
||||
total: boolean;
|
||||
};
|
||||
};
|
||||
reverseGeocoding: {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AssetEntity, AssetType } from '../entities';
|
||||
@Injectable()
|
||||
export class MetricsRepository implements IMetricsRepository {
|
||||
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
|
||||
async sendMetrics(payload: MetricsDto): Promise<void> {
|
||||
async sendMetrics(payload: Partial<MetricsDto>): Promise<void> {
|
||||
await axios.post('IMMICH-DATA-DOMAIN', payload);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class MetricsRepository implements IMetricsRepository {
|
||||
return os.cpus()[0].model;
|
||||
}
|
||||
|
||||
getMemoryCount() {
|
||||
getMemory() {
|
||||
return os.totalmem();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export class AppService {
|
||||
[JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data),
|
||||
[JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data),
|
||||
[JobName.METADATA_EXTRACTION]: (data) => this.metadataService.handleMetadataExtraction(data),
|
||||
[JobName.METRICS]: (data) => this.metricsService.handleMetrics(data),
|
||||
[JobName.METRICS]: () => this.metricsService.handleSendMetrics(),
|
||||
[JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataService.handleLivePhotoLinking(data),
|
||||
[JobName.QUEUE_RECOGNIZE_FACES]: (data) => this.personService.handleQueueRecognizeFaces(data),
|
||||
[JobName.RECOGNIZE_FACES]: (data) => this.personService.handleRecognizeFaces(data),
|
||||
|
||||
Reference in New Issue
Block a user