mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(server): add immich.users.total metric (#21780)
* Add immich.users.total metric * Fix tests & one lint error * Lint * Fix SQL Schema checks * Fix nit * Use workers argument in OnEvent hook and remove condition from method body
This commit is contained in:
parent
cf60f4cdcd
commit
b2d00405f1
9 changed files with 41 additions and 4 deletions
|
|
@ -19,6 +19,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
import { teardownTelemetry, TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||||
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { CliService } from 'src/services/cli.service';
|
import { CliService } from 'src/services/cli.service';
|
||||||
|
|
@ -55,6 +56,7 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||||
private jobService: JobService,
|
private jobService: JobService,
|
||||||
private telemetryRepository: TelemetryRepository,
|
private telemetryRepository: TelemetryRepository,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private userRepository: UserRepository,
|
||||||
) {
|
) {
|
||||||
logger.setAppName(this.worker);
|
logger.setAppName(this.worker);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,14 @@ group by
|
||||||
order by
|
order by
|
||||||
"user"."createdAt" asc
|
"user"."createdAt" asc
|
||||||
|
|
||||||
|
-- UserRepository.getCount
|
||||||
|
select
|
||||||
|
count(*) as "count"
|
||||||
|
from
|
||||||
|
"user"
|
||||||
|
where
|
||||||
|
"user"."deletedAt" is null
|
||||||
|
|
||||||
-- UserRepository.updateUsage
|
-- UserRepository.updateUsage
|
||||||
update "user"
|
update "user"
|
||||||
set
|
set
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,16 @@ export class UserRepository {
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql()
|
||||||
|
async getCount(): Promise<number> {
|
||||||
|
const result = await this.db
|
||||||
|
.selectFrom('user')
|
||||||
|
.select((eb) => eb.fn.countAll().as('count'))
|
||||||
|
.where('user.deletedAt', 'is', null)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
return Number(result.count);
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] })
|
||||||
async updateUsage(id: string, delta: number): Promise<void> {
|
async updateUsage(id: string, delta: number): Promise<void> {
|
||||||
await this.db
|
await this.db
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ export class BaseService {
|
||||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.telemetryRepository.api.addToGauge(`immich.users.total`, 1);
|
||||||
return this.userRepository.create(payload);
|
return this.userRepository.create(payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,7 @@ export class UserAdminService extends BaseService {
|
||||||
|
|
||||||
const status = force ? UserStatus.Removing : UserStatus.Deleted;
|
const status = force ? UserStatus.Removing : UserStatus.Deleted;
|
||||||
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
const user = await this.userRepository.update(id, { status, deletedAt: new Date() });
|
||||||
|
this.telemetryRepository.api.addToGauge(`immich.users.total`, -1);
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
|
await this.jobRepository.queue({ name: JobName.UserDelete, data: { id: user.id, force } });
|
||||||
|
|
@ -114,6 +115,7 @@ export class UserAdminService extends BaseService {
|
||||||
await this.findOrFail(id, { withDeleted: true });
|
await this.findOrFail(id, { withDeleted: true });
|
||||||
await this.albumRepository.restoreAll(id);
|
await this.albumRepository.restoreAll(id);
|
||||||
const user = await this.userRepository.restore(id);
|
const user = await this.userRepository.restore(id);
|
||||||
|
this.telemetryRepository.api.addToGauge('immich.users.total', 1);
|
||||||
return mapUserAdmin(user);
|
return mapUserAdmin(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ import { Updateable } from 'kysely';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
import { OnboardingDto, OnboardingResponseDto } from 'src/dtos/onboarding.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
|
||||||
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||||
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { CacheControl, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
import { CacheControl, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder, UserMetadataKey } from 'src/enum';
|
||||||
import { UserFindOptions } from 'src/repositories/user.repository';
|
import { UserFindOptions } from 'src/repositories/user.repository';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
@ -213,6 +213,12 @@ export class UserService extends BaseService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.Api] })
|
||||||
|
async onBootstrap(): Promise<void> {
|
||||||
|
const userCount = await this.userRepository.getCount();
|
||||||
|
this.telemetryRepository.api.addToGauge('immich.users.total', userCount);
|
||||||
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
@OnJob({ name: JobName.UserSyncUsage, queue: QueueName.BackgroundTask })
|
||||||
async handleUserSyncUsage(): Promise<JobStatus> {
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||||
await this.userRepository.syncUsage();
|
await this.userRepository.syncUsage();
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
|
||||||
import { SyncRepository } from 'src/repositories/sync.repository';
|
import { SyncRepository } from 'src/repositories/sync.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
|
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
|
|
@ -54,6 +55,7 @@ import { StackTable } from 'src/schema/tables/stack.table';
|
||||||
import { UserTable } from 'src/schema/tables/user.table';
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
|
import { BASE_SERVICE_DEPENDENCIES, BaseService } from 'src/services/base.service';
|
||||||
import { SyncService } from 'src/services/sync.service';
|
import { SyncService } from 'src/services/sync.service';
|
||||||
|
import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock';
|
||||||
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
import { factory, newDate, newEmbedding, newUuid } from 'test/small.factory';
|
||||||
import { automock, wait } from 'test/utils';
|
import { automock, wait } from 'test/utils';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
@ -347,6 +349,10 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||||
return automock(key);
|
return automock(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case TelemetryRepository: {
|
||||||
|
return newTelemetryRepositoryMock();
|
||||||
|
}
|
||||||
|
|
||||||
case DatabaseRepository: {
|
case DatabaseRepository: {
|
||||||
return automock(DatabaseRepository, {
|
return automock(DatabaseRepository, {
|
||||||
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
|
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { SessionRepository } from 'src/repositories/session.repository';
|
import { SessionRepository } from 'src/repositories/session.repository';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
|
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AuthService } from 'src/services/auth.service';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
|
|
@ -32,7 +33,7 @@ const setup = (db?: Kysely<DB>) => {
|
||||||
SystemMetadataRepository,
|
SystemMetadataRepository,
|
||||||
UserRepository,
|
UserRepository,
|
||||||
],
|
],
|
||||||
mock: [LoggingRepository, StorageRepository, EventRepository],
|
mock: [LoggingRepository, StorageRepository, EventRepository, TelemetryRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
|
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { UserService } from 'src/services/user.service';
|
import { UserService } from 'src/services/user.service';
|
||||||
|
|
@ -21,7 +22,7 @@ const setup = (db?: Kysely<DB>) => {
|
||||||
return newMediumService(UserService, {
|
return newMediumService(UserService, {
|
||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
|
real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository],
|
||||||
mock: [LoggingRepository, JobRepository],
|
mock: [LoggingRepository, JobRepository, TelemetryRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue